seawolf2357 commited on
Commit
1f54339
ยท
verified ยท
1 Parent(s): e006e27

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +888 -1084
app.py CHANGED
@@ -1,1103 +1,907 @@
1
- """
2
- HWP ํŒŒ์ผ ๋ณ€ํ™˜๊ธฐ + LLM ์ฑ„ํŒ… - Gradio ์›น ์•ฑ
3
- - Tab 1: LLM ์ฑ„ํŒ… (์ŠคํŠธ๋ฆฌ๋ฐ, ํŒŒ์ผ ์ฒจ๋ถ€ ์ง€์›)
4
- - Tab 2: HWP ๋ณ€ํ™˜๊ธฐ
5
- """
6
- import gradio as gr
7
- import tempfile
8
  import os
9
- import subprocess
10
- import shutil
11
- import sys
12
- import re
13
  import json
14
- import uuid
15
- import sqlite3
16
- import base64
17
  import requests
18
- import zlib
19
- import zipfile
20
- from pathlib import Path
21
- from datetime import datetime
22
- from typing import Generator, List, Dict, Optional
23
- from xml.etree import ElementTree as ET
24
-
25
- # ============== ํ™˜๊ฒฝ ์„ค์ • ==============
26
- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
27
- PYHWP_PATH = os.path.join(SCRIPT_DIR, 'pyhwp')
28
- DB_PATH = os.path.join(SCRIPT_DIR, 'chat_history.db')
29
-
30
- if os.path.exists(PYHWP_PATH):
31
- sys.path.insert(0, PYHWP_PATH)
32
-
33
- # ============== ๋ชจ๋“ˆ ์ž„ํฌํŠธ ==============
34
- try:
35
- import olefile
36
- OLEFILE_AVAILABLE = True
37
- print("olefile loaded successfully")
38
- except ImportError:
39
- OLEFILE_AVAILABLE = False
40
- print("olefile not available")
41
-
42
- try:
43
- from markdownify import markdownify as md
44
- MARKDOWNIFY_AVAILABLE = True
45
- print("markdownify loaded successfully")
46
- except ImportError:
47
- MARKDOWNIFY_AVAILABLE = False
48
-
49
- try:
50
- import html2text
51
- HTML2TEXT_AVAILABLE = True
52
- print("html2text loaded successfully")
53
- except ImportError:
54
- HTML2TEXT_AVAILABLE = False
55
-
56
- try:
57
- from bs4 import BeautifulSoup
58
- BS4_AVAILABLE = True
59
- except ImportError:
60
- BS4_AVAILABLE = False
61
-
62
- try:
63
- import PyPDF2
64
- PYPDF2_AVAILABLE = True
65
- print("PyPDF2 loaded successfully")
66
- except ImportError:
67
- PYPDF2_AVAILABLE = False
68
 
69
- try:
70
- import pdfplumber
71
- PDFPLUMBER_AVAILABLE = True
72
- print("pdfplumber loaded successfully")
73
- except ImportError:
74
- PDFPLUMBER_AVAILABLE = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- # hwp5txt ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ
77
- HWP5TXT_AVAILABLE = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  try:
79
- result = subprocess.run(['hwp5txt', '--help'], capture_output=True, timeout=5)
80
- if result.returncode == 0:
81
- HWP5TXT_AVAILABLE = True
82
- print("hwp5txt command available")
83
- except:
84
- pass
85
-
86
- if not HWP5TXT_AVAILABLE:
87
- try:
88
- result = subprocess.run([sys.executable, '-c', 'from hwp5.hwp5txt import main; print("ok")'],
89
- capture_output=True, timeout=5)
90
- if b'ok' in result.stdout:
91
- HWP5TXT_AVAILABLE = True
92
- print("hwp5txt module available")
93
- except:
94
- pass
95
-
96
- print(f"HWP5TXT_AVAILABLE: {HWP5TXT_AVAILABLE}")
97
-
98
- # ============== API ํ‚ค ์„ค์ • ==============
99
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
100
- FIREWORKS_API_KEY = os.environ.get("FIREWORKS_API_KEY", "")
101
-
102
- # ============== SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ==============
103
- def init_database():
104
- conn = sqlite3.connect(DB_PATH)
105
- cursor = conn.cursor()
106
- cursor.execute('''
107
- CREATE TABLE IF NOT EXISTS sessions (
108
- session_id TEXT PRIMARY KEY,
109
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
110
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
111
- title TEXT
112
- )
113
- ''')
114
- cursor.execute('''
115
- CREATE TABLE IF NOT EXISTS messages (
116
- id INTEGER PRIMARY KEY AUTOINCREMENT,
117
- session_id TEXT,
118
- role TEXT,
119
- content TEXT,
120
- file_info TEXT,
121
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
122
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
123
- )
124
- ''')
125
- conn.commit()
126
- conn.close()
127
-
128
- def create_session() -> str:
129
- session_id = str(uuid.uuid4())
130
- conn = sqlite3.connect(DB_PATH)
131
- cursor = conn.cursor()
132
- cursor.execute("INSERT INTO sessions (session_id, title) VALUES (?, ?)",
133
- (session_id, f"๋Œ€ํ™” {datetime.now().strftime('%Y-%m-%d %H:%M')}"))
134
- conn.commit()
135
- conn.close()
136
- return session_id
137
-
138
- def save_message(session_id: str, role: str, content: str, file_info: str = None):
139
- conn = sqlite3.connect(DB_PATH)
140
- cursor = conn.cursor()
141
- cursor.execute("INSERT INTO messages (session_id, role, content, file_info) VALUES (?, ?, ?, ?)",
142
- (session_id, role, content, file_info))
143
- cursor.execute("UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE session_id = ?", (session_id,))
144
- conn.commit()
145
- conn.close()
146
-
147
- def get_session_messages(session_id: str, limit: int = 20) -> List[Dict]:
148
- conn = sqlite3.connect(DB_PATH)
149
- cursor = conn.cursor()
150
- cursor.execute("SELECT role, content, file_info, created_at FROM messages WHERE session_id = ? ORDER BY created_at DESC LIMIT ?",
151
- (session_id, limit))
152
- rows = cursor.fetchall()
153
- conn.close()
154
- return [{"role": r[0], "content": r[1], "file_info": r[2], "created_at": r[3]} for r in reversed(rows)]
155
-
156
- def get_all_sessions() -> List[Dict]:
157
- conn = sqlite3.connect(DB_PATH)
158
- cursor = conn.cursor()
159
- cursor.execute("SELECT session_id, title, created_at, updated_at FROM sessions ORDER BY updated_at DESC LIMIT 50")
160
- rows = cursor.fetchall()
161
- conn.close()
162
- return [{"session_id": r[0], "title": r[1], "created_at": r[2], "updated_at": r[3]} for r in rows]
163
-
164
- def update_session_title(session_id: str, title: str):
165
- conn = sqlite3.connect(DB_PATH)
166
- cursor = conn.cursor()
167
- cursor.execute("UPDATE sessions SET title = ? WHERE session_id = ?", (title, session_id))
168
- conn.commit()
169
- conn.close()
170
-
171
- init_database()
172
-
173
- # ============== ํŒŒ์ผ ์œ ํ‹ธ๋ฆฌํ‹ฐ ==============
174
- def extract_text_from_pdf(file_path: str) -> str:
175
- text_parts = []
176
- if PDFPLUMBER_AVAILABLE:
177
- try:
178
- with pdfplumber.open(file_path) as pdf:
179
- for page in pdf.pages:
180
- text = page.extract_text()
181
- if text:
182
- text_parts.append(text)
183
- if text_parts:
184
- return "\n\n".join(text_parts)
185
- except Exception as e:
186
- print(f"pdfplumber error: {e}")
187
-
188
- if PYPDF2_AVAILABLE:
189
- try:
190
- with open(file_path, 'rb') as f:
191
- reader = PyPDF2.PdfReader(f)
192
- for page in reader.pages:
193
- text = page.extract_text()
194
- if text:
195
- text_parts.append(text)
196
- if text_parts:
197
- return "\n\n".join(text_parts)
198
- except Exception as e:
199
- print(f"PyPDF2 error: {e}")
200
- return None
201
-
202
- def extract_text_from_txt(file_path: str) -> str:
203
- for encoding in ['utf-8', 'euc-kr', 'cp949', 'utf-16', 'latin-1']:
204
- try:
205
- with open(file_path, 'r', encoding=encoding) as f:
206
- return f.read()
207
- except:
208
- continue
209
- return None
210
-
211
- def image_to_base64(file_path: str) -> str:
212
- with open(file_path, 'rb') as f:
213
- return base64.b64encode(f.read()).decode('utf-8')
214
-
215
- def get_image_mime_type(file_path: str) -> str:
216
- ext = Path(file_path).suffix.lower()
217
- return {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
218
- '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp'}.get(ext, 'image/jpeg')
219
-
220
- def is_image_file(fp: str) -> bool:
221
- return Path(fp).suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']
222
-
223
- def is_hwp_file(fp: str) -> bool:
224
- return Path(fp).suffix.lower() == '.hwp'
225
-
226
- def is_hwpx_file(fp: str) -> bool:
227
- return Path(fp).suffix.lower() == '.hwpx'
228
-
229
- def is_pdf_file(fp: str) -> bool:
230
- return Path(fp).suffix.lower() == '.pdf'
231
-
232
- def is_text_file(fp: str) -> bool:
233
- return Path(fp).suffix.lower() in ['.txt', '.md', '.json', '.csv', '.xml', '.html', '.css', '.js', '.py']
234
-
235
- # ============== HWPX ํ…์ŠคํŠธ ์ถ”์ถœ (ZIP/XML ๊ธฐ๋ฐ˜) ==============
236
-
237
- def extract_text_from_hwpx(file_path: str) -> tuple:
238
- """HWPX ํŒŒ์ผ์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ (ZIP ๋‚ด๋ถ€ XML ํŒŒ์‹ฑ)"""
239
- try:
240
- text_parts = []
241
-
242
- with zipfile.ZipFile(file_path, 'r') as zf:
243
- # HWPX ๋‚ด๋ถ€ ๊ตฌ์กฐ ํ™•์ธ
244
- file_list = zf.namelist()
245
- print(f" HWPX ๋‚ด๋ถ€ ํŒŒ์ผ: {file_list[:10]}...")
246
-
247
- # Contents ํด๋” ๋‚ด์˜ section XML ํŒŒ์ผ๋“ค ์ฒ˜๋ฆฌ
248
- section_files = sorted([f for f in file_list if f.startswith('Contents/section') and f.endswith('.xml')])
249
-
250
- if not section_files:
251
- # ๋‹ค๋ฅธ ๊ฒฝ๋กœ ์‹œ๋„
252
- section_files = sorted([f for f in file_list if 'section' in f.lower() and f.endswith('.xml')])
253
-
254
- print(f" ์„น์…˜ ํŒŒ์ผ: {section_files}")
255
-
256
- for section_file in section_files:
257
- try:
258
- with zf.open(section_file) as sf:
259
- content = sf.read()
260
-
261
- # XML ํŒŒ์‹ฑ
262
- try:
263
- # ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ œ๊ฑฐํ•˜๊ณ  ํŒŒ์‹ฑ
264
- content_str = content.decode('utf-8')
265
- # ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ œ๊ฑฐ
266
- content_str = re.sub(r'\sxmlns[^"]*"[^"]*"', '', content_str)
267
- content_str = re.sub(r'<[a-zA-Z]+:', '<', content_str)
268
- content_str = re.sub(r'</[a-zA-Z]+:', '</', content_str)
269
-
270
- root = ET.fromstring(content_str)
271
-
272
- # ๋ชจ๋“  ํ…์ŠคํŠธ ์ถ”์ถœ
273
- texts = []
274
- for elem in root.iter():
275
- # t ํƒœ๊ทธ (ํ…์ŠคํŠธ)
276
- if elem.tag.endswith('t') or elem.tag == 't':
277
- if elem.text:
278
- texts.append(elem.text)
279
- # ๋‹ค๋ฅธ ํ…์ŠคํŠธ ๋…ธ๋“œ
280
- elif elem.text and elem.text.strip():
281
- # ํƒœ๊ทธ ์ด๋ฆ„์ด ํ…์ŠคํŠธ ๊ด€๋ จ์ธ ๊ฒฝ์šฐ
282
- if any(x in elem.tag.lower() for x in ['text', 'run', 'para', 'char']):
283
- texts.append(elem.text.strip())
284
-
285
- if texts:
286
- text_parts.append(' '.join(texts))
287
-
288
- except ET.ParseError as e:
289
- print(f" XML ํŒŒ์‹ฑ ์˜ค๋ฅ˜ {section_file}: {e}")
290
- # ์ •๊ทœ์‹์œผ๋กœ ํ…์ŠคํŠธ ์ถ”์ถœ ์‹œ๋„
291
- text_matches = re.findall(r'>([^<]+)<', content.decode('utf-8', errors='ignore'))
292
- clean_texts = [t.strip() for t in text_matches if t.strip() and len(t.strip()) > 1]
293
- if clean_texts:
294
- text_parts.append(' '.join(clean_texts))
295
-
296
- except Exception as e:
297
- print(f" ์„น์…˜ ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜ {section_file}: {e}")
298
- continue
299
-
300
- # header.xml์—์„œ๋„ ํ…์ŠคํŠธ ์ถ”์ถœ ์‹œ๋„
301
- for header_file in [f for f in file_list if 'header' in f.lower() and f.endswith('.xml')]:
302
- try:
303
- with zf.open(header_file) as hf:
304
- content = hf.read().decode('utf-8', errors='ignore')
305
- text_matches = re.findall(r'>([^<]+)<', content)
306
- clean_texts = [t.strip() for t in text_matches if t.strip() and len(t.strip()) > 1]
307
- # ํ—ค๋”๋Š” ์งง์€ ํ…์ŠคํŠธ๋งŒ ์ถ”๊ฐ€
308
- if clean_texts:
309
- text_parts.insert(0, ' '.join(clean_texts[:5]))
310
- except:
311
- pass
312
-
313
- if text_parts:
314
- result = '\n\n'.join(text_parts)
315
- # ์ •๋ฆฌ
316
- result = re.sub(r'\s+', ' ', result)
317
- result = re.sub(r'\n{3,}', '\n\n', result)
318
- return result.strip(), None
319
-
320
- return None, "HWPX์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
321
-
322
- except zipfile.BadZipFile:
323
- return None, "์œ ํšจํ•˜์ง€ ์•Š์€ HWPX ํŒŒ์ผ"
324
- except Exception as e:
325
- return None, f"HWPX ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {str(e)}"
326
-
327
- # ============== HWP ํ…์ŠคํŠธ ์ถ”์ถœ (OLE ๊ธฐ๋ฐ˜) ==============
328
-
329
- def extract_text_with_hwp5txt(file_path: str) -> tuple:
330
- """hwp5txt๋กœ ํ…์ŠคํŠธ ์ถ”์ถœ"""
331
-
332
- # ๋ฐฉ๋ฒ• 1: hwp5txt ๋ช…๋ น์–ด ์ง์ ‘ ์‹คํ–‰
333
- try:
334
- result = subprocess.run(['hwp5txt', file_path], capture_output=True, timeout=60)
335
- if result.returncode == 0 and result.stdout:
336
- for enc in ['utf-8', 'cp949', 'euc-kr']:
337
- try:
338
- text = result.stdout.decode(enc)
339
- if text.strip() and len(text.strip()) > 10:
340
- return text.strip(), None
341
- except:
342
- continue
343
- except FileNotFoundError:
344
- pass
345
- except Exception as e:
346
- print(f" hwp5txt ๋ช…๋ น์–ด ์˜ค๋ฅ˜: {e}")
347
-
348
- # ๋ฐฉ๋ฒ• 2: Python ๋ชจ๋“ˆ๋กœ ์‹คํ–‰
349
- try:
350
- from hwp5.hwp5txt import main as hwp5txt_main
351
- from hwp5.hwp5txt import extract_text
352
- from hwp5.filestructure import Hwp5File
353
-
354
- hwp5file = Hwp5File(file_path)
355
- texts = []
356
-
357
- for section_idx in hwp5file.bodytext.sections():
358
- section = hwp5file.bodytext.section(section_idx)
359
- for para in extract_text(section):
360
- if para.strip():
361
- texts.append(para.strip())
362
-
363
- hwp5file.close()
364
-
365
- if texts:
366
- return '\n'.join(texts), None
367
-
368
- except ImportError:
369
- pass
370
- except Exception as e:
371
- print(f" hwp5txt ๋ชจ๋“ˆ ์˜ค๋ฅ˜: {e}")
372
-
373
- # ๋ฐฉ๋ฒ• 3: ์„œ๋ธŒํ”„๋กœ์„ธ์Šค๋กœ Python ์ฝ”๋“œ ์‹คํ–‰
374
- try:
375
- code = f'''
376
- import sys
377
- sys.path.insert(0, "{PYHWP_PATH}")
378
- from hwp5.filestructure import Hwp5File
379
- from hwp5.hwp5txt import extract_text
380
- hwp = Hwp5File("{file_path}")
381
- for idx in hwp.bodytext.sections():
382
- section = hwp.bodytext.section(idx)
383
- for para in extract_text(section):
384
- if para.strip():
385
- print(para.strip())
386
- hwp.close()
387
- '''
388
- result = subprocess.run([sys.executable, '-c', code], capture_output=True, timeout=60)
389
- if result.returncode == 0 and result.stdout:
390
- for enc in ['utf-8', 'cp949', 'euc-kr']:
391
- try:
392
- text = result.stdout.decode(enc)
393
- if text.strip() and len(text.strip()) > 10:
394
- return text.strip(), None
395
- except:
396
- continue
397
- except Exception as e:
398
- print(f" hwp5txt ์„œ๋ธŒํ”„๋กœ์„ธ์Šค ์˜ค๋ฅ˜: {e}")
399
-
400
- return None, "hwp5txt ์‹คํŒจ"
401
-
402
- def extract_text_with_olefile(file_path: str) -> tuple:
403
- """olefile์„ ์‚ฌ์šฉํ•œ HWP ํ…์ŠคํŠธ ์ถ”์ถœ"""
404
- if not OLEFILE_AVAILABLE:
405
- return None, "olefile ๋ชจ๋“ˆ ์—†์Œ"
406
-
407
- try:
408
- ole = olefile.OleFileIO(file_path)
409
-
410
- # ํŒŒ์ผ ํ—ค๋” ํ™•์ธ
411
- if not ole.exists('FileHeader'):
412
- ole.close()
413
- return None, "HWP ํŒŒ์ผ ํ—ค๋” ์—†์Œ"
414
-
415
- # ์••์ถ• ์—ฌ๋ถ€ ํ™•์ธ
416
- header_data = ole.openstream('FileHeader').read()
417
- is_compressed = (header_data[36] & 1) == 1 if len(header_data) > 36 else True
418
- print(f" HWP ์••์ถ• ์—ฌ๋ถ€: {is_compressed}")
419
-
420
- all_texts = []
421
-
422
- # BodyText ์„น์…˜๋“ค ์ฒ˜๋ฆฌ
423
- for entry in ole.listdir():
424
- entry_path = '/'.join(entry)
425
-
426
- if entry_path.startswith('BodyText/Section'):
427
- try:
428
- stream_data = ole.openstream(entry).read()
429
-
430
- # ์••์ถ• ํ•ด์ œ
431
- if is_compressed:
432
- try:
433
- stream_data = zlib.decompress(stream_data, -15)
434
- except:
435
- try:
436
- stream_data = zlib.decompress(stream_data)
437
- except:
438
- pass
439
-
440
- # ๋ ˆ์ฝ”๋“œ์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ
441
- section_text = extract_hwp_section_text(stream_data)
442
- if section_text:
443
- all_texts.append(section_text)
444
-
445
- except Exception as e:
446
- print(f" ์„น์…˜ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜ {entry_path}: {e}")
447
- continue
448
-
449
- ole.close()
450
-
451
- if all_texts:
452
- result = '\n\n'.join(all_texts)
453
- return result.strip(), None
454
-
455
- return None, "ํ…์ŠคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
456
-
457
- except Exception as e:
458
- return None, f"olefile ์˜ค๋ฅ˜: {str(e)}"
459
-
460
- def extract_hwp_section_text(data: bytes) -> str:
461
- """HWP ์„น์…˜ ๋ฐ์ดํ„ฐ์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ"""
462
- texts = []
463
- pos = 0
464
-
465
- while pos < len(data) - 4:
466
- try:
467
- # ๋ ˆ์ฝ”๋“œ ํ—ค๋” ์ฝ๊ธฐ
468
- header = int.from_bytes(data[pos:pos+4], 'little')
469
- tag_id = header & 0x3FF
470
- level = (header >> 10) & 0x3FF
471
- size = (header >> 20) & 0xFFF
472
-
473
- pos += 4
474
-
475
- # ํ™•์žฅ ํฌ๊ธฐ
476
- if size == 0xFFF:
477
- if pos + 4 > len(data):
478
- break
479
- size = int.from_bytes(data[pos:pos+4], 'little')
480
- pos += 4
481
-
482
- if pos + size > len(data):
483
- break
484
-
485
- record_data = data[pos:pos+size]
486
- pos += size
487
-
488
- # HWPTAG_PARA_TEXT = 67
489
- if tag_id == 67 and size > 0:
490
- text = decode_para_text(record_data)
491
- if text:
492
- texts.append(text)
493
-
494
- except:
495
- pos += 1
496
- continue
497
-
498
- return '\n'.join(texts) if texts else None
499
-
500
- def decode_para_text(data: bytes) -> str:
501
- """PARA_TEXT ๋ ˆ์ฝ”๋“œ ๋””์ฝ”๋”ฉ"""
502
- result = []
503
- i = 0
504
-
505
- while i < len(data) - 1:
506
- code = int.from_bytes(data[i:i+2], 'little')
507
-
508
- if code == 0:
509
- pass
510
- elif code == 1: # ํ™•์žฅ ์ปจํŠธ๋กค
511
- i += 14
512
- elif code == 2: # ์„น์…˜ ์ •์˜
513
- i += 14
514
- elif code == 3: # ํ•„๋“œ ์‹œ์ž‘
515
- i += 14
516
- elif code == 4: # ํ•„๋“œ ๋
517
- pass
518
- elif code == 9: # ํƒญ
519
- result.append('\t')
520
- elif code == 10: # ์ค„๋ฐ”๊ฟˆ
521
- result.append('\n')
522
- elif code == 13: # ๋ฌธ๋‹จ ๋
523
- result.append('\n')
524
- elif code == 24: # ํ•˜์ดํ”ˆ
525
- result.append('-')
526
- elif code == 30 or code == 31: # ๋นˆ์นธ
527
- result.append(' ')
528
- elif code < 32: # ๊ธฐํƒ€ ์ปจํŠธ๋กค ๋ฌธ์ž
529
- pass
530
  else:
531
- # ์ผ๋ฐ˜ ๋ฌธ์ž
532
- try:
533
- char = chr(code)
534
- if char.isprintable() or char in '\n\t ':
535
- result.append(char)
536
- except:
537
- pass
538
-
539
- i += 2
540
-
541
- text = ''.join(result).strip()
542
-
543
- # ์ •๋ฆฌ
544
- text = re.sub(r'[ \t]+', ' ', text)
545
- text = re.sub(r'\n{3,}', '\n\n', text)
546
-
547
- return text if len(text) > 2 else None
548
-
549
- def extract_text_from_hwp(file_path: str) -> tuple:
550
- """HWP ํŒŒ์ผ์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ (๋ฉ”์ธ ํ•จ์ˆ˜)"""
551
- print(f"\n[HWP ์ถ”์ถœ] ์‹œ์ž‘: {os.path.basename(file_path)}")
552
-
553
- # ๋ฐฉ๋ฒ• 1: hwp5txt
554
- print(" ๋ฐฉ๋ฒ• 1: hwp5txt...")
555
- text, error = extract_text_with_hwp5txt(file_path)
556
- if text and len(text.strip()) > 20:
557
- print(f" โœ“ hwp5txt ์„ฑ๊ณต: {len(text)} ๊ธ€์ž")
558
- return text, None
559
- print(f" โœ— hwp5txt ์‹คํŒจ: {error}")
560
-
561
- # ๋ฐฉ๋ฒ• 2: olefile
562
- print(" ๋ฐฉ๋ฒ• 2: olefile ํŒŒ์‹ฑ...")
563
- text, error = extract_text_with_olefile(file_path)
564
- if text and len(text.strip()) > 20:
565
- print(f" โœ“ olefile ์„ฑ๊ณต: {len(text)} ๊ธ€์ž")
566
- return text, None
567
- print(f" โœ— olefile ์‹คํŒจ: {error}")
568
-
569
- return None, "๋ชจ๋“  ์ถ”์ถœ ๋ฐฉ๋ฒ• ์‹คํŒจ"
570
-
571
- def extract_text_from_hwp_or_hwpx(file_path: str) -> tuple:
572
- """HWP ๋˜๋Š” HWPX ํŒŒ์ผ์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ"""
573
- if is_hwpx_file(file_path):
574
- print(f"\n[HWPX ์ถ”์ถœ] ์‹œ์ž‘: {os.path.basename(file_path)}")
575
- return extract_text_from_hwpx(file_path)
576
  else:
577
- return extract_text_from_hwp(file_path)
578
-
579
- # ============== HWP ๋ณ€ํ™˜ ํ•จ์ˆ˜๋“ค ==============
580
-
581
- def check_hwp_version(file_path):
582
- try:
583
- with open(file_path, 'rb') as f:
584
- header = f.read(32)
585
- if b'HWP Document File' in header:
586
- return "HWP v5", True
587
- elif header[:4] == b'\xd0\xcf\x11\xe0':
588
- return "HWP v5 (OLE)", True
589
- elif header[:4] == b'PK\x03\x04': # ZIP ํŒŒ์ผ (HWPX)
590
- return "HWPX", True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  else:
592
- return "Unknown", False
593
- except Exception as e:
594
- return f"Error: {e}", False
595
 
596
- def convert_to_html_subprocess(input_path, output_dir):
597
- """HTML ๋ณ€ํ™˜"""
598
- output_path = os.path.join(output_dir, "output.html")
599
-
600
- try:
601
- # hwp5html ์‹œ๋„
602
- for cmd in [['hwp5html', '--output', output_path, input_path],
603
- [sys.executable, '-c', f'from hwp5.hwp5html import main; import sys; sys.argv=["hwp5html","--output","{output_path}","{input_path}"]; main()']]:
604
  try:
605
- result = subprocess.run(cmd, capture_output=True, timeout=120)
606
- if result.returncode == 0:
607
- if os.path.exists(output_path):
608
- return output_path, None
609
- # ๋””๋ ‰ํ† ๋ฆฌ ๊ฒ€์ƒ‰
610
- for item in os.listdir(output_dir):
611
- item_path = os.path.join(output_dir, item)
612
- if item.lower().endswith(('.html', '.htm')):
613
- return item_path, None
614
- if os.path.isdir(item_path):
615
- return item_path, None
616
- except:
617
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
 
619
- except Exception as e:
620
- print(f"HTML ๋ณ€ํ™˜ ์˜ค๋ฅ˜: {e}")
621
-
622
- return None, "HTML ๋ณ€ํ™˜ ์‹คํŒจ"
623
-
624
- def convert_hwp_to_text(input_path: str) -> tuple:
625
- """HWP/HWPX๋ฅผ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜"""
626
- return extract_text_from_hwp_or_hwpx(input_path)
627
-
628
- def html_to_markdown(html_content):
629
- """HTML์„ Markdown์œผ๋กœ ๋ณ€ํ™˜"""
630
- if MARKDOWNIFY_AVAILABLE:
631
- try:
632
- return md(html_content, heading_style="ATX", bullets="-"), None
633
- except:
634
- pass
635
-
636
- if HTML2TEXT_AVAILABLE:
637
- try:
638
- h = html2text.HTML2Text()
639
- h.body_width = 0
640
- return h.handle(html_content), None
641
- except:
642
- pass
643
-
644
- if BS4_AVAILABLE:
 
 
 
645
  try:
646
- soup = BeautifulSoup(html_content, 'html.parser')
647
- return soup.get_text(separator='\n'), None
648
- except:
649
- pass
650
-
651
- return None, "Markdown ๋ณ€ํ™˜ ์‹คํŒจ"
652
-
653
- def convert_hwp_to_markdown(input_path: str) -> tuple:
654
- """HWP/HWPX๋ฅผ Markdown์œผ๋กœ ๋ณ€ํ™˜"""
655
- # ํ…์ŠคํŠธ ์ถ”์ถœ
656
- text, error = extract_text_from_hwp_or_hwpx(input_path)
657
- if text:
658
- return text, None
659
- return None, error
660
-
661
- # ============== LLM API ==============
662
-
663
- def call_groq_api_stream(messages: List[Dict], api_key: str) -> Generator[str, None, None]:
664
- if not api_key:
665
- yield "โŒ Groq API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
666
- return
667
-
668
- try:
669
- response = requests.post(
670
- "https://api.groq.com/openai/v1/chat/completions",
671
- headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
672
- json={
673
- "model": "meta-llama/llama-4-scout-17b-16e-instruct",
674
- "messages": messages,
675
- "temperature": 0.7,
676
- "max_tokens": 8192,
677
- "stream": True
678
- },
679
- stream=True
680
- )
681
-
682
- if response.status_code != 200:
683
- yield f"โŒ Groq API ์˜ค๋ฅ˜: {response.status_code}"
684
- return
685
-
686
- for line in response.iter_lines():
687
- if line:
688
- line = line.decode('utf-8')
689
- if line.startswith('data: ') and line[6:] != '[DONE]':
690
- try:
691
- data = json.loads(line[6:])
692
- content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
693
- if content:
694
- yield content
695
- except:
696
- continue
697
- except Exception as e:
698
- yield f"โŒ API ์˜ค๋ฅ˜: {str(e)}"
699
-
700
- def call_fireworks_api_stream(messages: List[Dict], image_base64: str, mime_type: str, api_key: str) -> Generator[str, None, None]:
701
- if not api_key:
702
- yield "โŒ Fireworks API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
703
- return
704
-
705
- try:
706
- formatted_messages = [{"role": m["role"], "content": m["content"]} for m in messages[:-1]]
707
- formatted_messages.append({
708
- "role": messages[-1]["role"],
709
- "content": [
710
- {"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{image_base64}"}},
711
- {"type": "text", "text": messages[-1]["content"]}
712
- ]
713
- })
714
-
715
- response = requests.post(
716
- "https://api.fireworks.ai/inference/v1/chat/completions",
717
- headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
718
- json={
719
- "model": "accounts/fireworks/models/qwen3-vl-235b-a22b-thinking",
720
- "max_tokens": 4096,
721
- "temperature": 0.6,
722
- "messages": formatted_messages,
723
- "stream": True
724
- },
725
- stream=True
726
- )
727
-
728
- if response.status_code != 200:
729
- yield f"โŒ Fireworks API ์˜ค๋ฅ˜: {response.status_code}"
730
- return
731
-
732
- for line in response.iter_lines():
733
- if line:
734
- line = line.decode('utf-8')
735
- if line.startswith('data: ') and line[6:] != '[DONE]':
736
- try:
737
- data = json.loads(line[6:])
738
- content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
739
- if content:
740
- yield content
741
- except:
742
- continue
743
- except Exception as e:
744
- yield f"โŒ API ์˜ค๋ฅ˜: {str(e)}"
745
-
746
- # ============== ์ฑ„ํŒ… ์ฒ˜๋ฆฌ ==============
747
-
748
- def process_file(file_path: str) -> tuple:
749
- if not file_path:
750
- return None, None, None
751
-
752
- filename = os.path.basename(file_path)
753
-
754
- if is_image_file(file_path):
755
- return "image", image_to_base64(file_path), get_image_mime_type(file_path)
756
-
757
- if is_hwp_file(file_path) or is_hwpx_file(file_path):
758
- text, error = extract_text_from_hwp_or_hwpx(file_path)
759
- if text and len(text.strip()) > 20:
760
- return "text", f"[ํ•œ๊ธ€ ๋ฌธ์„œ: {filename}]\n\n{text}", None
761
- return "error", f"ํ•œ๊ธ€ ๋ฌธ์„œ ์ถ”์ถœ ์‹คํŒจ: {error}", None
762
-
763
- if is_pdf_file(file_path):
764
- text = extract_text_from_pdf(file_path)
765
- if text:
766
- return "text", f"[PDF ๋ฌธ์„œ: {filename}]\n\n{text}", None
767
- return "error", "PDF ์ถ”์ถœ ์‹คํŒจ", None
768
-
769
- if is_text_file(file_path):
770
- text = extract_text_from_txt(file_path)
771
- if text:
772
- return "text", f"[ํ…์ŠคํŠธ ํŒŒ์ผ: {filename}]\n\n{text}", None
773
- return "error", "ํ…์ŠคํŠธ ์ฝ๊ธฐ ์‹คํŒจ", None
774
-
775
- return "unsupported", f"์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ˜•์‹: {filename}", None
776
-
777
- def chat_response(message: str, history: List[Dict], file: Optional[str],
778
- session_id: str, groq_key: str, fireworks_key: str) -> Generator[tuple, None, None]:
779
- if history is None:
780
- history = []
781
-
782
- if not message.strip() and not file:
783
- yield history, session_id
784
- return
785
-
786
- if not session_id:
787
- session_id = create_session()
788
-
789
- # ํŒŒ์ผ ์ฒ˜๋ฆฌ
790
- file_type, file_content, file_mime = None, None, None
791
- file_info = None
792
-
793
- if file:
794
- file_type, file_content, file_mime = process_file(file)
795
- file_info = json.dumps({"type": file_type, "filename": os.path.basename(file)})
796
-
797
- if file_type == "error":
798
- history = history + [
799
- {"role": "user", "content": message or "ํŒŒ์ผ ์—…๋กœ๋“œ"},
800
- {"role": "assistant", "content": f"โŒ {file_content}"}
801
- ]
802
- yield history, session_id
803
- return
804
- elif file_type == "unsupported":
805
- history = history + [
806
- {"role": "user", "content": message or "ํŒŒ์ผ ์—…๋กœ๋“œ"},
807
- {"role": "assistant", "content": f"โš ๏ธ {file_content}"}
808
- ]
809
- yield history, session_id
810
- return
811
-
812
- # ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€
813
- user_msg = message
814
- if file:
815
- filename = os.path.basename(file)
816
- user_msg = f"๐Ÿ“Ž {filename}\n\n{message}" if message else f"๐Ÿ“Ž {filename}"
817
-
818
- history = history + [{"role": "user", "content": user_msg}, {"role": "assistant", "content": ""}]
819
- yield history, session_id
820
-
821
- # API ๋ฉ”์‹œ์ง€ ๊ตฌ์„ฑ
822
- db_messages = get_session_messages(session_id, limit=10)
823
- api_messages = [{
824
- "role": "system",
825
- "content": "๋‹น์‹ ์€ ๋„์›€์ด ๋˜๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋Œ€ํ™”ํ•˜๋ฉฐ, ํŒŒ์ผ์ด ์ฒจ๋ถ€๋˜๋ฉด ๋‚ด์šฉ์„ ์ƒ์„ธํžˆ ๋ถ„์„ํ•˜์—ฌ ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค."
826
- }]
827
-
828
- for m in db_messages:
829
- api_messages.append({"role": m["role"], "content": m["content"]})
830
-
831
- current_content = message or ""
832
- if file_type == "text" and file_content:
833
- current_content = f"{file_content}\n\n์‚ฌ์šฉ์ž ์งˆ๋ฌธ: {message}" if message else f"{file_content}\n\n์œ„ ๋ฌธ์„œ ๋‚ด์šฉ์„ ์š”์•ฝํ•ด์ฃผ์„ธ์š”."
834
-
835
- api_messages.append({"role": "user", "content": current_content})
836
-
837
- # ์‘๋‹ต ์ƒ์„ฑ
838
- full_response = ""
839
- if file_type == "image":
840
- for chunk in call_fireworks_api_stream(api_messages, file_content, file_mime, fireworks_key):
841
- full_response += chunk
842
- history[-1] = {"role": "assistant", "content": full_response}
843
- yield history, session_id
844
  else:
845
- for chunk in call_groq_api_stream(api_messages, groq_key):
846
- full_response += chunk
847
- history[-1] = {"role": "assistant", "content": full_response}
848
- yield history, session_id
849
-
850
- # ์ €์žฅ
851
- save_message(session_id, "user", current_content, file_info)
852
- save_message(session_id, "assistant", full_response)
853
-
854
- if len(db_messages) == 0 and message:
855
- update_session_title(session_id, message[:50])
856
-
857
- def new_chat():
858
- return [], create_session(), None
859
-
860
- def load_session(session_id: str) -> tuple:
861
- if not session_id:
862
- return [], ""
863
- messages = get_session_messages(session_id, limit=50)
864
- return [{"role": m["role"], "content": m["content"]} for m in messages], session_id
865
-
866
- # ============== HWP ๋ณ€ํ™˜๊ธฐ (Tab 2) ==============
867
-
868
- def convert_to_odt_subprocess(input_path, output_dir):
869
- output_path = os.path.join(output_dir, "output.odt")
870
- try:
871
- for cmd in [['hwp5odt', '--output', output_path, input_path],
872
- [sys.executable, '-c', f'from hwp5.hwp5odt import main; import sys; sys.argv=["hwp5odt","--output","{output_path}","{input_path}"]; main()']]:
873
- try:
874
- result = subprocess.run(cmd, capture_output=True, timeout=120)
875
- if result.returncode == 0 and os.path.exists(output_path):
876
- return output_path, None
877
- except:
878
- continue
879
- except:
880
- pass
881
- return None, "ODT ๋ณ€ํ™˜ ์‹คํŒจ"
882
-
883
- def convert_to_xml_subprocess(input_path, output_dir):
884
- output_path = os.path.join(output_dir, "output.xml")
885
- try:
886
- for cmd in [['hwp5xml', input_path],
887
- [sys.executable, '-c', f'from hwp5.hwp5xml import main; import sys; sys.argv=["hwp5xml","{input_path}"]; main()']]:
888
- try:
889
- result = subprocess.run(cmd, capture_output=True, timeout=120)
890
- if result.returncode == 0 and result.stdout:
891
- with open(output_path, 'wb') as f:
892
- f.write(result.stdout)
893
- return output_path, None
894
- except:
895
- continue
896
- except:
897
- pass
898
- return None, "XML ๋ณ€ํ™˜ ์‹คํŒจ"
899
-
900
- def convert_hwp(file, output_format, progress=gr.Progress()):
901
- if not file:
902
- return None, "โŒ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.", ""
903
-
904
- input_file = file.name if hasattr(file, 'name') else str(file)
905
- ext_lower = Path(input_file).suffix.lower()
906
-
907
- if ext_lower not in ['.hwp', '.hwpx']:
908
- return None, "โŒ HWP ๋˜๋Š” HWPX ํŒŒ์ผ๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.", ""
909
-
910
- progress(0.1, desc="ํŒŒ์ผ ๋ถ„์„ ์ค‘...")
911
- version, is_valid = check_hwp_version(input_file)
912
- if not is_valid:
913
- return None, f"โŒ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ: {version}", ""
914
-
915
- tmp_dir = tempfile.mkdtemp()
916
-
917
- try:
918
- input_filename = os.path.basename(input_file)
919
- input_path = os.path.join(tmp_dir, input_filename)
920
- shutil.copy(input_file, input_path)
921
-
922
- progress(0.3, desc=f"{output_format}๋กœ ๋ณ€ํ™˜ ์ค‘...")
923
-
924
- output_path, error, ext = None, None, ""
925
-
926
- if output_format == "HTML":
927
- if ext_lower == '.hwpx':
928
- return None, "โŒ HWPX๋Š” HTML ๋ณ€ํ™˜์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. TXT๋‚˜ Markdown์„ ์‚ฌ์šฉํ•˜์„ธ์š”.", ""
929
- output_path, error = convert_to_html_subprocess(input_path, tmp_dir)
930
- ext = ".html"
931
- if output_path and os.path.isdir(output_path):
932
- zip_path = shutil.make_archive(os.path.join(tmp_dir, "html"), 'zip', output_path)
933
- output_path, ext = zip_path, ".zip"
934
-
935
- elif output_format == "ODT (OpenDocument)":
936
- if ext_lower == '.hwpx':
937
- return None, "โŒ HWPX๋Š” ODT ๋ณ€ํ™˜์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. TXT๋‚˜ Markdown์„ ์‚ฌ์šฉํ•˜์„ธ์š”.", ""
938
- output_path, error = convert_to_odt_subprocess(input_path, tmp_dir)
939
- ext = ".odt"
940
-
941
- elif output_format == "TXT (ํ…์ŠคํŠธ)":
942
- text, error = extract_text_from_hwp_or_hwpx(input_path)
943
- if text:
944
- output_path = os.path.join(tmp_dir, "output.txt")
945
- with open(output_path, 'w', encoding='utf-8') as f:
946
- f.write(text)
947
- ext = ".txt"
948
 
949
- elif output_format == "Markdown":
950
- text, error = convert_hwp_to_markdown(input_path)
951
- if text:
952
- output_path = os.path.join(tmp_dir, "output.md")
953
- with open(output_path, 'w', encoding='utf-8') as f:
954
- f.write(text)
955
- ext = ".md"
956
 
957
- elif output_format == "XML":
958
- if ext_lower == '.hwpx':
959
- # HWPX๋Š” ์ด๋ฏธ XML ๊ธฐ๋ฐ˜์ด๋ฏ€๋กœ ๋‚ด๋ถ€ XML ์ถ”์ถœ
960
- try:
961
- with zipfile.ZipFile(input_path, 'r') as zf:
962
- # ๋ชจ๋“  XML ํŒŒ์ผ์„ ํ•˜๋‚˜๋กœ ํ•ฉ์นจ
963
- xml_contents = []
964
- for name in zf.namelist():
965
- if name.endswith('.xml'):
966
- with zf.open(name) as f:
967
- xml_contents.append(f"<!-- {name} -->\n{f.read().decode('utf-8', errors='ignore')}")
968
-
969
- output_path = os.path.join(tmp_dir, "output.xml")
970
- with open(output_path, 'w', encoding='utf-8') as f:
971
- f.write('\n\n'.join(xml_contents))
972
- except Exception as e:
973
- error = f"HWPX XML ์ถ”์ถœ ์‹คํŒจ: {e}"
974
- else:
975
- output_path, error = convert_to_xml_subprocess(input_path, tmp_dir)
976
- ext = ".xml"
977
-
978
- if not output_path:
979
- return None, f"โŒ {error or '๋ณ€ํ™˜ ์‹คํŒจ'}", ""
980
-
981
- if not os.path.exists(output_path):
982
- return None, "โŒ ๋ณ€ํ™˜๋œ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", ""
983
-
984
- progress(0.8, desc="์™„๋ฃŒ...")
985
-
986
- base_name = Path(input_filename).stem
987
- final_output = os.path.join(tmp_dir, f"{base_name}{ext}")
988
- if output_path != final_output:
989
- shutil.copy2(output_path, final_output)
990
-
991
- file_size = os.path.getsize(final_output)
992
- size_str = f"{file_size/1024:.1f} KB" if file_size > 1024 else f"{file_size} bytes"
993
-
994
- preview = ""
995
- if ext in ['.txt', '.md', '.xml']:
996
- try:
997
- with open(final_output, 'r', encoding='utf-8', errors='ignore') as f:
998
- preview = f.read(5000)
999
- if len(preview) >= 5000:
1000
- preview += "\n\n... (์ƒ๋žต)"
1001
- except:
1002
- pass
1003
- elif ext == '.zip':
1004
- preview = "๐Ÿ“ฆ HTML์ด ZIP์œผ๋กœ ์••๏ฟฝ๏ฟฝ๏ฟฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
1005
-
1006
- progress(1.0, desc="์™„๋ฃŒ!")
1007
- return final_output, f"โœ… ๋ณ€ํ™˜ ์™„๋ฃŒ: {base_name}{ext} ({size_str})", preview
1008
-
1009
- except Exception as e:
1010
- import traceback
1011
- traceback.print_exc()
1012
- return None, f"โŒ ์˜ค๋ฅ˜: {str(e)}", ""
1013
-
1014
- # ============== Gradio UI ==============
1015
-
1016
- css = """
1017
- .upload-box { border: 2px dashed #6366f1 !important; border-radius: 12px !important; }
1018
- .download-box { border: 2px solid #22c55e !important; border-radius: 12px !important; }
1019
- """
1020
-
1021
- with gr.Blocks(title="AI ๋ฌธ์„œ ์–ด์‹œ์Šคํ„ดํŠธ") as demo:
1022
- session_state = gr.State("")
1023
-
1024
- gr.Markdown("# ๐Ÿค– AI ๋ฌธ์„œ ์–ด์‹œ์Šคํ„ดํŠธ\nLLM ์ฑ„ํŒ… + HWP/HWPX ๋ฌธ์„œ ๋ณ€ํ™˜")
1025
-
1026
- with gr.Tabs():
1027
- with gr.Tab("๐Ÿ’ฌ AI ์ฑ„ํŒ…"):
1028
- with gr.Row():
1029
- with gr.Column(scale=1):
1030
- gr.Markdown("### โš™๏ธ ์„ค์ •")
1031
- with gr.Accordion("๐Ÿ”‘ API ํ‚ค", open=True):
1032
- groq_key = gr.Textbox(label="Groq API Key", type="password", value=GROQ_API_KEY)
1033
- fireworks_key = gr.Textbox(label="Fireworks API Key", type="password", value=FIREWORKS_API_KEY)
1034
-
1035
- gr.Markdown("### ๐Ÿ“ ์ง€์› ํŒŒ์ผ\n- ์ด๋ฏธ์ง€: JPG, PNG\n- ๋ฌธ์„œ: PDF, TXT\n- ํ•œ๊ธ€: HWP, HWPX โœจ")
1036
- new_btn = gr.Button("๐Ÿ†• ์ƒˆ ๋Œ€ํ™”", variant="primary")
1037
-
1038
- with gr.Accordion("๐Ÿ“œ ๊ธฐ๋ก", open=False):
1039
- session_list = gr.Dataframe(headers=["ID", "์ œ๋ชฉ", "์‹œ๊ฐ„"], interactive=False)
1040
- refresh_btn = gr.Button("๐Ÿ”„ ์ƒˆ๋กœ๊ณ ์นจ", size="sm")
1041
-
1042
- with gr.Column(scale=3):
1043
- chatbot = gr.Chatbot(label="๋Œ€ํ™”", height=500)
1044
-
1045
- with gr.Row():
1046
- file_upload = gr.File(label="๐Ÿ“Ž ํŒŒ์ผ", file_types=[".jpg",".jpeg",".png",".gif",".webp",".pdf",".txt",".md",".hwp",".hwpx"], scale=1)
1047
- msg_input = gr.Textbox(placeholder="๋ฉ”์‹œ์ง€ ์ž…๋ ฅ...", lines=2, show_label=False, scale=4)
1048
-
1049
- with gr.Row():
1050
- submit_btn = gr.Button("๐Ÿ“ค ์ „์†ก", variant="primary", scale=3)
1051
- clear_btn = gr.Button("๐Ÿ—‘๏ธ ์ง€์šฐ๊ธฐ", scale=1)
1052
-
1053
- with gr.Tab("๐Ÿ“„ HWP ๋ณ€ํ™˜๊ธฐ"):
1054
- gr.Markdown("### HWP/HWPX ํŒŒ์ผ ๋ณ€ํ™˜๊ธฐ")
1055
  with gr.Row():
1056
- with gr.Column():
1057
- hwp_input = gr.File(label="HWP/HWPX ํŒŒ์ผ", file_types=[".hwp", ".hwpx"], elem_classes=["upload-box"])
1058
- format_select = gr.Radio(["HTML", "ODT (OpenDocument)", "TXT (ํ…์ŠคํŠธ)", "Markdown", "XML"], value="TXT (ํ…์ŠคํŠธ)", label="ํ˜•์‹")
1059
- convert_btn = gr.Button("๐Ÿ”„ ๋ณ€ํ™˜", variant="primary", size="lg")
1060
- with gr.Column():
1061
- status_out = gr.Textbox(label="์ƒํƒœ", interactive=False)
1062
- file_out = gr.File(label="๋‹ค์šด๋กœ๋“œ", elem_classes=["download-box"])
1063
 
1064
- with gr.Accordion("๐Ÿ“‹ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", open=False):
1065
- preview_out = gr.Textbox(lines=15, interactive=False)
 
1066
 
1067
- gr.Markdown("""
1068
- > **์ฐธ๊ณ **: HWPX ํŒŒ์ผ์€ TXT, Markdown, XML ๋ณ€ํ™˜๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.
1069
- """)
1070
-
1071
- # ์ด๋ฒคํŠธ
1072
- def on_submit(msg, hist, f, sid, gk, fk):
1073
- if hist is None: hist = []
1074
- for r in chat_response(msg, hist, f, sid, gk, fk):
1075
- yield r[0], r[1], "", None
1076
-
1077
- submit_btn.click(on_submit, [msg_input, chatbot, file_upload, session_state, groq_key, fireworks_key],
1078
- [chatbot, session_state, msg_input, file_upload])
1079
- msg_input.submit(on_submit, [msg_input, chatbot, file_upload, session_state, groq_key, fireworks_key],
1080
- [chatbot, session_state, msg_input, file_upload])
1081
-
1082
- new_btn.click(lambda: ([], create_session(), None, ""), outputs=[chatbot, session_state, file_upload, msg_input])
1083
- clear_btn.click(lambda: ([], None, ""), outputs=[chatbot, file_upload, msg_input])
1084
-
1085
- def refresh():
1086
- sessions = get_all_sessions()
1087
- return [[s["session_id"][:8], s["title"] or "์ œ๋ชฉ์—†์Œ", s["updated_at"][:16] if s["updated_at"] else ""] for s in sessions]
1088
-
1089
- refresh_btn.click(refresh, outputs=[session_list])
1090
-
1091
- def select_session(evt: gr.SelectData, data):
1092
- if evt.index[0] < len(data):
1093
- for s in get_all_sessions():
1094
- if s["session_id"].startswith(data[evt.index[0]][0]):
1095
- return load_session(s["session_id"])
1096
- return [], ""
1097
-
1098
- session_list.select(select_session, [session_list], [chatbot, session_state])
1099
- convert_btn.click(convert_hwp, [hwp_input, format_select], [file_out, status_out, preview_out])
1100
- demo.load(refresh, outputs=[session_list])
1101
-
1102
- if __name__ == "__main__":
1103
- demo.launch(css=css)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
 
 
 
 
2
  import json
3
+ import copy
4
+ import time
 
5
  import requests
6
+ import random
7
+ import logging
8
+ import numpy as np
9
+ import spaces
10
+ from typing import Any, Dict, List, Optional, Union
11
+
12
+ import torch
13
+ from PIL import Image
14
+ import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ from diffusers import (
17
+ DiffusionPipeline,
18
+ AutoencoderKL,
19
+ ZImagePipeline
20
+ )
21
+
22
+ from huggingface_hub import (
23
+ hf_hub_download,
24
+ HfFileSystem,
25
+ ModelCard,
26
+ snapshot_download)
27
+
28
+ from diffusers.utils import load_image
29
+ from typing import Iterable
30
+
31
+ # ============================================
32
+ # Comic Style CSS
33
+ # ============================================
34
+ COMIC_CSS = """
35
+ @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
36
+
37
+ .gradio-container {
38
+ background-color: #FEF9C3 !important;
39
+ background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
40
+ background-size: 20px 20px !important;
41
+ min-height: 100vh !important;
42
+ font-family: 'Comic Neue', cursive, sans-serif !important;
43
+ }
44
+
45
+ footer, .footer, .gradio-container footer, .built-with, [class*="footer"], .gradio-footer, a[href*="gradio.app"] {
46
+ display: none !important;
47
+ visibility: hidden !important;
48
+ height: 0 !important;
49
+ }
50
+
51
+ /* HOME Button Style */
52
+ .home-button-container {
53
+ display: flex;
54
+ justify-content: center;
55
+ align-items: center;
56
+ gap: 15px;
57
+ margin-bottom: 15px;
58
+ padding: 12px 20px;
59
+ background: linear-gradient(135deg, #10B981 0%, #059669 100%);
60
+ border: 4px solid #1F2937;
61
+ border-radius: 12px;
62
+ box-shadow: 6px 6px 0 #1F2937;
63
+ }
64
+
65
+ .home-button {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ gap: 8px;
69
+ padding: 10px 25px;
70
+ background: linear-gradient(135deg, #FACC15 0%, #F59E0B 100%);
71
+ color: #1F2937;
72
+ font-family: 'Bangers', cursive;
73
+ font-size: 1.4rem;
74
+ letter-spacing: 2px;
75
+ text-decoration: none;
76
+ border: 3px solid #1F2937;
77
+ border-radius: 8px;
78
+ box-shadow: 4px 4px 0 #1F2937;
79
+ transition: all 0.2s ease;
80
+ }
81
+
82
+ .home-button:hover {
83
+ background: linear-gradient(135deg, #FDE047 0%, #FACC15 100%);
84
+ transform: translate(-2px, -2px);
85
+ box-shadow: 6px 6px 0 #1F2937;
86
+ }
87
+
88
+ .home-button:active {
89
+ transform: translate(2px, 2px);
90
+ box-shadow: 2px 2px 0 #1F2937;
91
+ }
92
+
93
+ .url-display {
94
+ font-family: 'Comic Neue', cursive;
95
+ font-size: 1.1rem;
96
+ font-weight: 700;
97
+ color: #FFF;
98
+ background: rgba(0,0,0,0.3);
99
+ padding: 8px 16px;
100
+ border-radius: 6px;
101
+ border: 2px solid rgba(255,255,255,0.3);
102
+ }
103
+
104
+ .header-container {
105
+ text-align: center;
106
+ padding: 25px 20px;
107
+ background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
108
+ border: 4px solid #1F2937;
109
+ border-radius: 12px;
110
+ margin-bottom: 20px;
111
+ box-shadow: 8px 8px 0 #1F2937;
112
+ position: relative;
113
+ }
114
+
115
+ .header-title {
116
+ font-family: 'Bangers', cursive !important;
117
+ color: #FFF !important;
118
+ font-size: 2.8rem !important;
119
+ text-shadow: 3px 3px 0 #1F2937 !important;
120
+ letter-spacing: 3px !important;
121
+ margin: 0 !important;
122
+ }
123
+
124
+ .header-subtitle {
125
+ font-family: 'Comic Neue', cursive !important;
126
+ font-size: 1.1rem !important;
127
+ color: #FEF9C3 !important;
128
+ margin-top: 8px !important;
129
+ font-weight: 700 !important;
130
+ }
131
+
132
+ .stats-badge {
133
+ display: inline-block;
134
+ background: #FACC15;
135
+ color: #1F2937;
136
+ padding: 6px 14px;
137
+ border-radius: 20px;
138
+ font-size: 0.9rem;
139
+ margin: 3px;
140
+ font-weight: 700;
141
+ border: 2px solid #1F2937;
142
+ box-shadow: 2px 2px 0 #1F2937;
143
+ }
144
+
145
+ .gr-panel, .gr-box, .gr-form, .block, .gr-group {
146
+ background: #FFF !important;
147
+ border: 3px solid #1F2937 !important;
148
+ border-radius: 8px !important;
149
+ box-shadow: 5px 5px 0 #1F2937 !important;
150
+ }
151
+
152
+ .gr-button-primary, button.primary, .gr-button.primary {
153
+ background: linear-gradient(135deg, #EF4444 0%, #F97316 100%) !important;
154
+ border: 3px solid #1F2937 !important;
155
+ border-radius: 8px !important;
156
+ color: #FFF !important;
157
+ font-family: 'Bangers', cursive !important;
158
+ font-size: 1.3rem !important;
159
+ letter-spacing: 2px !important;
160
+ padding: 12px 24px !important;
161
+ box-shadow: 4px 4px 0 #1F2937 !important;
162
+ text-shadow: 1px 1px 0 #1F2937 !important;
163
+ transition: all 0.2s ease !important;
164
+ }
165
+
166
+ .gr-button-primary:hover, button.primary:hover {
167
+ background: linear-gradient(135deg, #DC2626 0%, #EA580C 100%) !important;
168
+ transform: translate(-2px, -2px) !important;
169
+ box-shadow: 6px 6px 0 #1F2937 !important;
170
+ }
171
+
172
+ .gr-button-primary:active, button.primary:active {
173
+ transform: translate(2px, 2px) !important;
174
+ box-shadow: 2px 2px 0 #1F2937 !important;
175
+ }
176
+
177
+ textarea, input[type="text"], input[type="number"] {
178
+ background: #FFF !important;
179
+ border: 3px solid #1F2937 !important;
180
+ border-radius: 8px !important;
181
+ color: #1F2937 !important;
182
+ font-family: 'Comic Neue', cursive !important;
183
+ font-weight: 700 !important;
184
+ }
185
+
186
+ textarea:focus, input[type="text"]:focus {
187
+ border-color: #3B82F6 !important;
188
+ box-shadow: 3px 3px 0 #3B82F6 !important;
189
+ }
190
+
191
+ .info-box {
192
+ background: linear-gradient(135deg, #FACC15 0%, #FDE047 100%) !important;
193
+ border: 3px solid #1F2937 !important;
194
+ border-radius: 8px !important;
195
+ padding: 12px 15px !important;
196
+ margin: 10px 0 !important;
197
+ box-shadow: 4px 4px 0 #1F2937 !important;
198
+ font-family: 'Comic Neue', cursive !important;
199
+ font-weight: 700 !important;
200
+ color: #1F2937 !important;
201
+ }
202
+
203
+ label, .gr-input-label, .gr-block-label {
204
+ color: #1F2937 !important;
205
+ font-family: 'Comic Neue', cursive !important;
206
+ font-weight: 700 !important;
207
+ }
208
+
209
+ .gr-accordion {
210
+ background: #E0F2FE !important;
211
+ border: 3px solid #1F2937 !important;
212
+ border-radius: 8px !important;
213
+ box-shadow: 4px 4px 0 #1F2937 !important;
214
+ }
215
+
216
+ .footer-comic {
217
+ text-align: center;
218
+ padding: 20px;
219
+ background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
220
+ border: 4px solid #1F2937;
221
+ border-radius: 12px;
222
+ margin-top: 20px;
223
+ box-shadow: 6px 6px 0 #1F2937;
224
+ }
225
+
226
+ .footer-comic p {
227
+ font-family: 'Comic Neue', cursive !important;
228
+ color: #FFF !important;
229
+ margin: 5px 0 !important;
230
+ font-weight: 700 !important;
231
+ }
232
+
233
+ ::-webkit-scrollbar {
234
+ width: 12px;
235
+ height: 12px;
236
+ }
237
+
238
+ ::-webkit-scrollbar-track {
239
+ background: #FEF9C3;
240
+ border: 2px solid #1F2937;
241
+ }
242
+
243
+ ::-webkit-scrollbar-thumb {
244
+ background: #3B82F6;
245
+ border: 2px solid #1F2937;
246
+ border-radius: 6px;
247
+ }
248
+
249
+ ::-webkit-scrollbar-thumb:hover {
250
+ background: #EF4444;
251
+ }
252
+
253
+ ::selection {
254
+ background: #FACC15;
255
+ color: #1F2937;
256
+ }
257
+
258
+ /* Slider Styling */
259
+ input[type="range"] {
260
+ accent-color: #3B82F6;
261
+ }
262
+
263
+ /* Image/Gallery Container */
264
+ .gr-image, .gr-gallery {
265
+ border: 3px solid #1F2937 !important;
266
+ border-radius: 8px !important;
267
+ box-shadow: 4px 4px 0 #1F2937 !important;
268
+ }
269
+
270
+ /* Original CSS additions */
271
+ #gen_btn{height: 100%}
272
+ #gen_column{align-self: stretch}
273
+ #title{text-align: center}
274
+ #title h1{font-size: 3em; display:inline-flex; align-items:center}
275
+ #title img{width: 100px; margin-right: 0.5em}
276
+ #gallery .grid-wrap{height: 10vh}
277
+ #lora_list{background: var(--block-background-fill);padding: 0 1em .3em; font-size: 90%}
278
+ .card_internal{display: flex;height: 100px;margin-top: .5em}
279
+ .card_internal img{margin-right: 1em}
280
+ .styler{--form-gap-width: 0px !important}
281
+ #progress{height:30px}
282
+ #progress .generating{display:none}
283
+ .progress-container {width: 100%;height: 30px;background-color: #f0f0f0;border-radius: 15px;overflow: hidden;margin-bottom: 20px}
284
+ .progress-bar {height: 100%;background-color: #4f46e5;width: calc(var(--current) / var(--total) * 100%);transition: width 0.5s ease-in-out}
285
+ """
286
 
287
+ loras = [
288
+ # ๋กœ์ปฌ jimin LoRA (app.py์™€ ๊ฐ™์€ ๋””๋ ‰ํ† ๋ฆฌ์— jimin.safetensors ํ•„์š”)
289
+ {
290
+ "image": "https://i.namu.wiki/i/VxF2jh087d4V6f9LWw1jQoLcQ_ymdhyD7XtRnq2KVYmN6DZsL4vCTrd1v-ubr8zfejyCCKvUWaBVf9JM9GqR271X6nh4e6mUbW11LaFr9QtepztFJeDZJ1VISkW5KBbebCpqv-w2Uv7RmMPwB5kacg.webp",
291
+
292
+ "title": "koyoon Style",
293
+ "repo": "./", # ๋กœ์ปฌ ๊ฒฝ๋กœ
294
+ "weights": "koyoon.safetensors",
295
+ "trigger_word": "koyoon"
296
+ },
297
+
298
+ {
299
+ "image": "https://i.namu.wiki/i/umL8EZtn0hs-nMRYeFxIrkGrMe-R1u5c9fJE8ufrLjvXz52VcSIbG7TT9QJoL2rR7vsFww1lLrE4bwfn5uOBzfq9a90HGdNdlTLmr_KoqOchTovbVC3RDzhDbp7FI-Wq-esCu7_BYIptqethL4onBg.webp",
300
+
301
+ "title": "jimin Style",
302
+ "repo": "./", # ๋กœ์ปฌ ๊ฒฝ๋กœ
303
+ "weights": "jimin.safetensors",
304
+ "trigger_word": "jimin"
305
+ },
306
+ {
307
+ "image": "https://huggingface.co/strangerzonehf/Flux-Ultimate-LoRA-Collection/resolve/main/images/1111111111.png",
308
+ "title": "AWPortrait Z",
309
+ "repo": "Shakker-Labs/AWPortrait-Z", #1
310
+ "weights": "AWPortrait-Z.safetensors",
311
+ "trigger_word": "Portrait"
312
+ },
313
+
314
+ {
315
+ "image": "https://cdn-uploads.huggingface.co/production/uploads/653cd3049107029eb004f968/DLCGlF9uUnFo5zxR5uyx6.png",
316
+ "title": "50s Western",
317
+ "repo": "neph1/50s_western_lora_zit",
318
+ "weights": "50s_western_z_100.safetensors",
319
+ "trigger_word": "50s_western"
320
+ },
321
+
322
+ {
323
+ "image": "https://huggingface.co/neph1/80s_scifi_lora_zit/resolve/main/images/ComfyUI_10288_.png",
324
+ "title": "80s Scifi",
325
+ "repo": "neph1/80s_scifi_lora_zit",
326
+ "weights": "80s_scifi_z_80.safetensors",
327
+ "trigger_word": "80s_scifi"
328
+ },
329
+
330
+ # --------------------------------------------------------------------------------------------------------------------------------------
331
+ {
332
+ "image": "https://huggingface.co/Ttio2/Z-Image-Turbo-pencil-sketch/resolve/main/images/z-image_00097_.png",
333
+ "title": "Turbo Pencil",
334
+ "repo": "Ttio2/Z-Image-Turbo-pencil-sketch", #0
335
+ "weights": "Zimage_pencil_sketch.safetensors",
336
+ "trigger_word": "pencil sketch"
337
+ },
338
+ {
339
+ "image": "https://huggingface.co/neph1/50s_scifi_lora_zit/resolve/main/images/ComfyUI_08067_.png",
340
+ "title": "50s Scifi",
341
+ "repo": "neph1/50s_scifi_lora_zit",
342
+ "weights": "50s_scifi_z_80.safetensors",
343
+ "trigger_word": "50s_scifi"
344
+ },
345
+ {
346
+ "image": "https://huggingface.co/strangerzonehf/Flux-Ultimate-LoRA-Collection/resolve/main/images/cookie-mons.png",
347
+ "title": "Yarn Art Style",
348
+ "repo": "linoyts/yarn-art-style", #28
349
+ "weights": "yarn-art-style_000001250.safetensors",
350
+ "trigger_word": "yarn art style"
351
+ },
352
+ {
353
+ "image": "https://huggingface.co/Quorlen/Z-Image-Turbo-Behind-Reeded-Glass-Lora/resolve/main/images/ComfyUI_00391_.png",
354
+ "title": "Behind Reeded Glass",
355
+ "repo": "Quorlen/Z-Image-Turbo-Behind-Reeded-Glass-Lora", #26
356
+ "weights": "Z_Image_Turbo_Behind_Reeded_Glass_Lora_TAV2_000002750.safetensors",
357
+ "trigger_word": "Act1vate!, Behind reeded glass"
358
+ },
359
+ {
360
+ "image": "https://huggingface.co/ostris/z_image_turbo_childrens_drawings/resolve/main/images/1764433619736__000003000_9.jpg",
361
+ "title": "Childrens Drawings",
362
+ "repo": "ostris/z_image_turbo_childrens_drawings", #2
363
+ "weights": "z_image_turbo_childrens_drawings.safetensors",
364
+ "trigger_word": "Children Drawings"
365
+ },
366
+ {
367
+ "image": "https://huggingface.co/strangerzonehf/Flux-Ultimate-LoRA-Collection/resolve/main/images/xcxc.png",
368
+ "title": "Tarot Z",
369
+ "repo": "multimodalart/tarot-z-image-lora", #22
370
+ "weights": "tarot-z-image_000001250.safetensors",
371
+ "trigger_word": "trtcrd"
372
+ },
373
+ {
374
+ "image": "https://huggingface.co/renderartist/Technically-Color-Z-Image-Turbo/resolve/main/images/ComfyUI_00917_.png",
375
+ "title": "Technically Color Z",
376
+ "repo": "renderartist/Technically-Color-Z-Image-Turbo", #3
377
+ "weights": "Technically_Color_Z_Image_Turbo_v1_renderartist_2000.safetensors",
378
+ "trigger_word": "t3chnic4lly"
379
+ },
380
+ {
381
+ "image": "https://huggingface.co/SkyAsl/Tattoo-artist-Z/resolve/main/images/a%20dragon%20with%20flames.png",
382
+ "title": "Tattoo-artist-Z",
383
+ "repo": "SkyAsl/Tattoo-artist-Z", #31
384
+ "weights": "adapter_model.safetensors",
385
+ "trigger_word": "a tattoo design"
386
+ },
387
+ {
388
+ "image": "https://huggingface.co/strangerzonehf/Flux-Ultimate-LoRA-Collection/resolve/main/images/z-image_00147_.png",
389
+ "title": "Turbo Ghibli",
390
+ "repo": "Ttio2/Z-Image-Turbo-Ghibli-Style", #19
391
+ "weights": "ghibli_zimage_finetune.safetensors",
392
+ "trigger_word": "Ghibli Style"
393
+ },
394
+ {
395
+ "image": "https://huggingface.co/tarn59/pixel_art_style_lora_z_image_turbo/resolve/main/images/ComfyUI_00273_.png",
396
+ "title": "Pixel Art",
397
+ "repo": "tarn59/pixel_art_style_lora_z_image_turbo", #4
398
+ "weights": "pixel_art_style_z_image_turbo.safetensors",
399
+ "trigger_word": "Pixel art style."
400
+ },
401
+ {
402
+ "image": "https://huggingface.co/renderartist/Saturday-Morning-Z-Image-Turbo/resolve/main/images/Saturday_Morning_Z_15.png",
403
+ "title": "Saturday Morning",
404
+ "repo": "renderartist/Saturday-Morning-Z-Image-Turbo", #5
405
+ "weights": "Saturday_Morning_Z_Image_Turbo_v1_renderartist_1250.safetensors",
406
+ "trigger_word": "saturd4ym0rning"
407
+ },
408
+ {
409
+ "image": "https://huggingface.co/AIImageStudio/ReversalFilmGravure_z_Image_turbo/resolve/main/images/2025-12-01_173047-z_image_z_image_turbo_bf16-435125750859057-euler_10_hires.png",
410
+ "title": "ReversalFilmGravure",
411
+ "repo": "AIImageStudio/ReversalFilmGravure_z_Image_turbo", #6
412
+ "weights": "z_image_turbo_ReversalFilmGravure_v1.0.safetensors",
413
+ "trigger_word": "Reversal Film Gravure, analog film photography"
414
+ },
415
+ {
416
+ "image": "https://huggingface.co/renderartist/Coloring-Book-Z-Image-Turbo-LoRA/resolve/main/images/CBZ_00274_.png",
417
+ "title": "Coloring Book Z",
418
+ "repo": "renderartist/Coloring-Book-Z-Image-Turbo-LoRA", #7
419
+ "weights": "Coloring_Book_Z_Image_Turbo_v1_renderartist_2000.safetensors",
420
+ "trigger_word": "c0l0ringb00k"
421
+ },
422
+ {
423
+ "image": "https://huggingface.co/damnthatai/1950s_American_Dream/resolve/main/images/ZImage_20251129163459_135x_00001_.jpg",
424
+ "title": "1950s American Dream",
425
+ "repo": "damnthatai/1950s_American_Dream", #8
426
+ "weights": "5os4m3r1c4n4_z.safetensors",
427
+ "trigger_word": "5os4m3r1c4n4, 1950s, painting, a painting of"
428
+ },
429
+ {
430
+ "image": "https://huggingface.co/wcde/Z-Image-Turbo-DeJPEG-Lora/resolve/main/images/01.png",
431
+ "title": "DeJPEG",
432
+ "repo": "wcde/Z-Image-Turbo-DeJPEG-Lora", #9
433
+ "weights": "dejpeg_v3.safetensors",
434
+ "trigger_word": ""
435
+ },
436
+ {
437
+ "image": "https://huggingface.co/suayptalha/Z-Image-Turbo-Realism-LoRA/resolve/main/images/n4aSpqa-YFXYo4dtcIg4W.png",
438
+ "title": "DeJPEG",
439
+ "repo": "suayptalha/Z-Image-Turbo-Realism-LoRA", #10
440
+ "weights": "pytorch_lora_weights.safetensors",
441
+ "trigger_word": "Realism"
442
+ },
443
+ {
444
+ "image": "https://huggingface.co/renderartist/Classic-Painting-Z-Image-Turbo-LoRA/resolve/main/images/Classic_Painting_Z_00247_.png",
445
+ "title": "Classic Painting Z",
446
+ "repo": "renderartist/Classic-Painting-Z-Image-Turbo-LoRA", #11
447
+ "weights": "Classic_Painting_Z_Image_Turbo_v1_renderartist_1750.safetensors",
448
+ "trigger_word": "class1cpa1nt"
449
+ },
450
+ {
451
+ "image": "https://huggingface.co/DK9/3D_MMORPG_style_z-image-turbo_lora/resolve/main/images/10_with_lora.png",
452
+ "title": "3D MMORPG",
453
+ "repo": "DK9/3D_MMORPG_style_z-image-turbo_lora", #12
454
+ "weights": "lostark_v1.safetensors",
455
+ "trigger_word": ""
456
+ },
457
+ {
458
+ "image": "https://huggingface.co/Danrisi/Olympus_UltraReal_ZImage/resolve/main/images/Z-Image_01011_.png",
459
+ "title": "Olympus UltraReal",
460
+ "repo": "Danrisi/Olympus_UltraReal_ZImage", #13
461
+ "weights": "Olympus.safetensors",
462
+ "trigger_word": "digital photography, early 2000s compact camera aesthetic, amateur candid shot, digital photography, early 2000s compact camera aesthetic, amateur candid shot, direct flash lighting, hard flash shadow, specular highlights, overexposed highlights"
463
+ },
464
+ {
465
+ "image": "https://huggingface.co/AiAF/D-ART_Z-Image-Turbo_LoRA/resolve/main/images/example_l3otpwzaz.png",
466
+ "title": "D ART Z Image",
467
+ "repo": "AiAF/D-ART_Z-Image-Turbo_LoRA", #14
468
+ "weights": "D-ART_Z-Image-Turbo.safetensors",
469
+ "trigger_word": "D-ART"
470
+ },
471
+ {
472
+ "image": "https://huggingface.co/AlekseyCalvin/Marionette_Modernism_Z-image-Turbo_LoRA/resolve/main/bluebirdmandoll.webp",
473
+ "title": "Marionette Modernism",
474
+ "repo": "AlekseyCalvin/Marionette_Modernism_Z-image-Turbo_LoRA", #15
475
+ "weights": "ZImageDadadoll_000003600.safetensors",
476
+ "trigger_word": "DADADOLL style"
477
+ },
478
+ {
479
+ "image": "https://huggingface.co/AlekseyCalvin/HistoricColor_Z-image-Turbo-LoRA/resolve/main/HSTZgen2.webp",
480
+ "title": "Historic Color Z",
481
+ "repo": "AlekseyCalvin/HistoricColor_Z-image-Turbo-LoRA", #16
482
+ "weights": "ZImage1HST_000004000.safetensors",
483
+ "trigger_word": "HST style"
484
+ },
485
+ {
486
+ "image": "https://huggingface.co/tarn59/80s_air_brush_style_z_image_turbo/resolve/main/images/ComfyUI_00707_.png",
487
+ "title": "80s Air Brush",
488
+ "repo": "tarn59/80s_air_brush_style_z_image_turbo", #17
489
+ "weights": "80s_air_brush_style_v2_z_image_turbo.safetensors",
490
+ "trigger_word": "80s Air Brush style."
491
+ },
492
+ {
493
+ "image": "https://huggingface.co/CedarC/Z-Image_360/resolve/main/images/1765505225357__000006750_6.jpg",
494
+ "title": "360panorama",
495
+ "repo": "CedarC/Z-Image_360", #18
496
+ "weights": "Z-Image_360.safetensors",
497
+ "trigger_word": "360panorama"
498
+ },
499
+ {
500
+ "image": "https://huggingface.co/HAV0X1014/Z-Image-Turbo-KF-Bat-Eared-Fox-LoRA/resolve/main/images/ComfyUI_00132_.png",
501
+ "title": "KF-Bat-Eared",
502
+ "repo": "HAV0X1014/Z-Image-Turbo-KF-Bat-Eared-Fox-LoRA", #21
503
+ "weights": "z-image-turbo-bat_eared_fox.safetensors",
504
+ "trigger_word": "bat_eared_fox_kemono_friends"
505
+ },
506
+ {
507
+ "image": "https://cdn-uploads.huggingface.co/production/uploads/653cd3049107029eb004f968/IHttgddXu6ZBMo7eyy8p6.png",
508
+ "title": "80s Horror",
509
+ "repo": "neph1/80s_horror_movies_lora_zit", #23
510
+ "weights": "80s_horror_z_80.safetensors",
511
+ "trigger_word": "80s_horror"
512
+ },
513
+ {
514
+ "image": "https://huggingface.co/Quorlen/z_image_turbo_Sunbleached_Protograph_Style_Lora/resolve/main/images/ComfyUI_00024_.png",
515
+ "title": "Sunbleached Protograph",
516
+ "repo": "Quorlen/z_image_turbo_Sunbleached_Protograph_Style_Lora", #24
517
+ "weights": "zimageturbo_Sunbleach_Photograph_Style_Lora_TAV2_000002750.safetensors",
518
+ "trigger_word": "Act1vate!"
519
+ },
520
+ {
521
+ "image": "https://huggingface.co/bunnycore/Z-Art-2.1/resolve/main/images/ComfyUI_00069_.png",
522
+ "title": "Z-Art-2.1",
523
+ "repo": "bunnycore/Z-Art-2.1", #25
524
+ "weights": "Z-Image-Art2.1.safetensors",
525
+ "trigger_word": "anime art"
526
+ },
527
+ {
528
+ "image": "https://huggingface.co/cactusfriend/longfurby-z/resolve/main/images/1764658860954__000003000_1.jpg",
529
+ "title": "Longfurby",
530
+ "repo": "cactusfriend/longfurby-z", #27
531
+ "weights": "longfurbyZ.safetensors",
532
+ "trigger_word": ""
533
+ },
534
+ {
535
+ "image": "https://huggingface.co/SkyAsl/Pixel-artist-Z/resolve/main/pixel-art-result.png",
536
+ "title": "Pixel Art",
537
+ "repo": "SkyAsl/Pixel-artist-Z", #29
538
+ "weights": "adapter_model.safetensors",
539
+ "trigger_word": "a pixel art character"
540
+ },
541
+ ]
542
+
543
+ dtype = torch.bfloat16
544
+ device = "cuda" if torch.cuda.is_available() else "cpu"
545
+ base_model = "Tongyi-MAI/Z-Image-Turbo"
546
+
547
+ print(f"Loading {base_model} pipeline...")
548
+
549
+ # Initialize Pipeline
550
+ pipe = ZImagePipeline.from_pretrained(
551
+ base_model,
552
+ torch_dtype=dtype,
553
+ low_cpu_mem_usage=False,
554
+ ).to(device)
555
+
556
+ # ======== AoTI compilation + FA3 ========
557
+ # As per reference for optimization
558
  try:
559
+ print("Applying AoTI compilation and FA3...")
560
+ pipe.transformer.layers._repeated_blocks = ["ZImageTransformerBlock"]
561
+ spaces.aoti_blocks_load(pipe.transformer.layers, "zerogpu-aoti/Z-Image", variant="fa3")
562
+ print("Optimization applied successfully.")
563
+ except Exception as e:
564
+ print(f"Optimization warning: {e}. Continuing with standard pipeline.")
565
+
566
+ MAX_SEED = np.iinfo(np.int32).max
567
+
568
+ class calculateDuration:
569
+ def __init__(self, activity_name=""):
570
+ self.activity_name = activity_name
571
+
572
+ def __enter__(self):
573
+ self.start_time = time.time()
574
+ return self
575
+
576
+ def __exit__(self, exc_type, exc_value, traceback):
577
+ self.end_time = time.time()
578
+ self.elapsed_time = self.end_time - self.start_time
579
+ if self.activity_name:
580
+ print(f"Elapsed time for {self.activity_name}: {self.elapsed_time:.6f} seconds")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
  else:
582
+ print(f"Elapsed time: {self.elapsed_time:.6f} seconds")
583
+
584
+ def update_selection(evt: gr.SelectData, width, height):
585
+ selected_lora = loras[evt.index]
586
+ new_placeholder = f"Type a prompt for {selected_lora['title']}"
587
+ lora_repo = selected_lora["repo"]
588
+ # ๋กœ์ปฌ LoRA ์ฒ˜๋ฆฌ
589
+ if lora_repo == "./":
590
+ updated_text = f"### Selected: Local LoRA - {selected_lora['title']} โœ…"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  else:
592
+ updated_text = f"### Selected: [{lora_repo}](https://huggingface.co/{lora_repo}) โœ…"
593
+
594
+ # Default aspect ratio
595
+ aspect = "1:1 (Instagram Square)"
596
+ width = 1024
597
+ height = 1024
598
+
599
+ if "aspect" in selected_lora:
600
+ if selected_lora["aspect"] == "portrait":
601
+ aspect = "9:16 (Instagram Reels/TikTok/Shorts)"
602
+ width = 768
603
+ height = 1344
604
+ elif selected_lora["aspect"] == "landscape":
605
+ aspect = "16:9 (YouTube/Twitter/X)"
606
+ width = 1344
607
+ height = 768
608
+
609
+ return (
610
+ gr.update(placeholder=new_placeholder),
611
+ updated_text,
612
+ evt.index,
613
+ aspect,
614
+ width,
615
+ height,
616
+ )
617
+
618
+ @spaces.GPU
619
+ def run_lora(prompt, image_input, image_strength, cfg_scale, steps, selected_index, randomize_seed, seed, width, height, lora_scale, progress=gr.Progress(track_tqdm=True)):
620
+ # Clean up previous LoRAs in both cases
621
+ with calculateDuration("Unloading LoRA"):
622
+ pipe.unload_lora_weights()
623
+
624
+ # Check if a LoRA is selected
625
+ if selected_index is not None and selected_index < len(loras):
626
+ selected_lora = loras[selected_index]
627
+ lora_path = selected_lora["repo"]
628
+ trigger_word = selected_lora["trigger_word"]
629
+
630
+ # Prepare Prompt with Trigger Word
631
+ if(trigger_word):
632
+ if "trigger_position" in selected_lora:
633
+ if selected_lora["trigger_position"] == "prepend":
634
+ prompt_mash = f"{trigger_word} {prompt}"
635
+ else:
636
+ prompt_mash = f"{prompt} {trigger_word}"
637
  else:
638
+ prompt_mash = f"{trigger_word} {prompt}"
639
+ else:
640
+ prompt_mash = prompt
641
 
642
+ # Load LoRA
643
+ with calculateDuration(f"Loading LoRA weights for {selected_lora['title']}"):
644
+ weight_name = selected_lora.get("weights", None)
 
 
 
 
 
645
  try:
646
+ pipe.load_lora_weights(
647
+ lora_path,
648
+ weight_name=weight_name,
649
+ adapter_name="default",
650
+ low_cpu_mem_usage=True
651
+ )
652
+ # Set adapter scale
653
+ pipe.set_adapters(["default"], adapter_weights=[lora_scale])
654
+ except Exception as e:
655
+ print(f"Error loading LoRA: {e}")
656
+ gr.Warning("Failed to load LoRA weights. Generating with base model.")
657
+ else:
658
+ # Base Model Case
659
+ print("No LoRA selected. Running with Base Model.")
660
+ prompt_mash = prompt
661
+
662
+ with calculateDuration("Randomizing seed"):
663
+ if randomize_seed:
664
+ seed = random.randint(0, MAX_SEED)
665
+
666
+ generator = torch.Generator(device=device).manual_seed(seed)
667
+
668
+ # Note: Z-Image-Turbo is strictly T2I in this reference implementation.
669
+ # Img2Img via image_input is disabled/ignored for this pipeline update.
670
+
671
+ with calculateDuration("Generating image"):
672
+ # For Turbo models, guidance_scale is typically 0.0
673
+ forced_guidance = 0.0 # Turbo mode
674
+
675
+ final_image = pipe(
676
+ prompt=prompt_mash,
677
+ height=int(height),
678
+ width=int(width),
679
+ num_inference_steps=int(steps),
680
+ guidance_scale=forced_guidance,
681
+ generator=generator,
682
+ ).images[0]
683
+
684
+ yield final_image, seed, gr.update(visible=False)
685
+
686
+ def get_huggingface_safetensors(link):
687
+ split_link = link.split("/")
688
+ if(len(split_link) == 2):
689
+ model_card = ModelCard.load(link)
690
+ base_model = model_card.data.get("base_model")
691
+ print(base_model)
692
+
693
+ # Relaxed check to allow Z-Image or Flux or others, assuming user knows what they are doing
694
+ # or specifically check for Z-Image-Turbo
695
+ if base_model not in ["Tongyi-MAI/Z-Image-Turbo", "black-forest-labs/FLUX.1-dev"]:
696
+ # Just a warning instead of error to allow experimentation
697
+ print("Warning: Base model might not match.")
698
 
699
+ image_path = model_card.data.get("widget", [{}])[0].get("output", {}).get("url", None)
700
+ trigger_word = model_card.data.get("instance_prompt", "")
701
+ image_url = f"https://huggingface.co/{link}/resolve/main/{image_path}" if image_path else None
702
+ fs = HfFileSystem()
703
+ try:
704
+ list_of_files = fs.ls(link, detail=False)
705
+ for file in list_of_files:
706
+ if(file.endswith(".safetensors")):
707
+ safetensors_name = file.split("/")[-1]
708
+ if (not image_url and file.lower().endswith((".jpg", ".jpeg", ".png", ".webp"))):
709
+ image_elements = file.split("/")
710
+ image_url = f"https://huggingface.co/{link}/resolve/main/{image_elements[-1]}"
711
+ except Exception as e:
712
+ print(e)
713
+ gr.Warning(f"You didn't include a link neither a valid Hugging Face repository with a *.safetensors LoRA")
714
+ raise Exception(f"You didn't include a link neither a valid Hugging Face repository with a *.safetensors LoRA")
715
+ return split_link[1], link, safetensors_name, trigger_word, image_url
716
+
717
+ def check_custom_model(link):
718
+ if(link.startswith("https://")):
719
+ if(link.startswith("https://huggingface.co") or link.startswith("https://www.huggingface.co")):
720
+ link_split = link.split("huggingface.co/")
721
+ return get_huggingface_safetensors(link_split[1])
722
+ else:
723
+ return get_huggingface_safetensors(link)
724
+
725
+ def add_custom_lora(custom_lora):
726
+ global loras
727
+ if(custom_lora):
728
  try:
729
+ title, repo, path, trigger_word, image = check_custom_model(custom_lora)
730
+ print(f"Loaded custom LoRA: {repo}")
731
+ card = f'''
732
+ <div class="custom_lora_card">
733
+ <span>Loaded custom LoRA:</span>
734
+ <div class="card_internal">
735
+ <img src="{image}" />
736
+ <div>
737
+ <h3>{title}</h3>
738
+ <small>{"Using: <code><b>"+trigger_word+"</code></b> as the trigger word" if trigger_word else "No trigger word found. If there's a trigger word, include it in your prompt"}<br></small>
739
+ </div>
740
+ </div>
741
+ </div>
742
+ '''
743
+ existing_item_index = next((index for (index, item) in enumerate(loras) if item['repo'] == repo), None)
744
+ if(not existing_item_index):
745
+ new_item = {
746
+ "image": image,
747
+ "title": title,
748
+ "repo": repo,
749
+ "weights": path,
750
+ "trigger_word": trigger_word
751
+ }
752
+ print(new_item)
753
+ existing_item_index = len(loras)
754
+ loras.append(new_item)
755
+
756
+ return gr.update(visible=True, value=card), gr.update(visible=True), gr.Gallery(selected_index=None), f"Custom: {path}", existing_item_index, trigger_word
757
+ except Exception as e:
758
+ gr.Warning(f"Invalid LoRA: either you entered an invalid link, or a non-supported LoRA")
759
+ return gr.update(visible=True, value=f"Invalid LoRA: either you entered an invalid link, a non-supported LoRA"), gr.update(visible=False), gr.update(), "", None, ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  else:
761
+ return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None, ""
762
+
763
+ def remove_custom_lora():
764
+ return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None, ""
765
+
766
+ run_lora.zerogpu = True
767
+
768
+ with gr.Blocks(title="Z-IMAGE GEN/LORA", delete_cache=(60, 60)) as demo:
769
+ gr.LoginButton(value="Option: HuggingFace 'Login' for extra GPU quota +", size="sm")
770
+ # HOME Button
771
+ gr.HTML("""
772
+ <div class="home-button-container">
773
+ <a href="https://www.humangen.ai" target="_blank" class="home-button">
774
+ ๐Ÿ  HOME
775
+ </a>
776
+ <span class="url-display">๐ŸŒ www.humangen.ai</span>
777
+ </div>
778
+ """)
779
+
780
+ # Header
781
+ gr.HTML("""
782
+ <div class="header-container">
783
+ <div class="header-title">๐ŸŽจ Z-IMAGE GEN/LORA ๐ŸŽจ</div>
784
+ <div class="header-subtitle">Generate amazing images with Z-Image Turbo and various LoRA styles!</div>
785
+ <div style="margin-top:12px">
786
+ <span class="stats-badge">โšก Turbo Speed</span>
787
+ <span class="stats-badge">๐ŸŽญ 30+ LoRAs</span>
788
+ <span class="stats-badge">๐Ÿ–ผ๏ธ High Quality</span>
789
+ <span class="stats-badge">๐Ÿ”ง Custom LoRA</span>
790
+ </div>
791
+ </div>
792
+ """)
793
+
794
+ selected_index = gr.State(None)
795
+ with gr.Row():
796
+ with gr.Column(scale=3):
797
+ prompt = gr.Textbox(label="Enter Prompt", lines=1, placeholder="โœฆ๏ธŽ Choose the LoRA and type the prompt (LoRA = None โ†’ Base Model = Active)")
798
+ with gr.Column(scale=1, elem_id="gen_column"):
799
+ generate_button = gr.Button("๐Ÿš€ Generate", variant="primary", elem_id="gen_btn")
800
+ with gr.Row():
801
+ with gr.Column():
802
+ selected_info = gr.Markdown("### No LoRA Selected (Base Model)")
803
+ gallery = gr.Gallery(
804
+ [(item["image"], item["title"]) for item in loras],
805
+ label="Z-Image LoRAs",
806
+ allow_preview=False,
807
+ columns=3,
808
+ elem_id="gallery",
809
+ )
810
+ with gr.Group():
811
+ custom_lora = gr.Textbox(label="Enter Custom LoRA", placeholder="Paste the LoRA path and press Enter (e.g., Shakker-Labs/AWPortrait-Z).")
812
+ gr.Markdown("[Check the list of Z-Image LoRA's](https://huggingface.co/models?other=base_model:adapter:Tongyi-MAI/Z-Image-Turbo)", elem_id="lora_list")
813
+ custom_lora_info = gr.HTML(visible=False)
814
+ custom_lora_button = gr.Button("Remove custom LoRA", visible=False)
815
+ with gr.Column():
816
+ progress_bar = gr.Markdown(elem_id="progress",visible=False)
817
+ result = gr.Image(label="Generated Image", format="png", height=630)
818
+
819
+ # SNS Aspect Ratio Presets
820
+ ASPECT_RATIOS = {
821
+ "1:1 (Instagram Square)": (1024, 1024),
822
+ "9:16 (Instagram Reels/TikTok/Shorts)": (768, 1344),
823
+ "16:9 (YouTube/Twitter/X)": (1344, 768),
824
+ "4:5 (Instagram Portrait)": (896, 1120),
825
+ "5:4 (Instagram Landscape)": (1120, 896),
826
+ "3:4 (Portrait Photo)": (896, 1152),
827
+ "4:3 (Landscape Photo)": (1152, 896),
828
+ "2:3 (Pinterest)": (832, 1248),
829
+ "3:2 (Classic Photo)": (1248, 832),
830
+ "21:9 (Cinematic Ultra-wide)": (1344, 576),
831
+ "9:21 (Tall Banner)": (576, 1344),
832
+ }
833
+
834
+ def update_size(aspect_ratio):
835
+ width, height = ASPECT_RATIOS.get(aspect_ratio, (1024, 1024))
836
+ return width, height
837
+
838
+ with gr.Row():
839
+ with gr.Accordion("โš™๏ธ Advanced Settings", open=True):
840
+ with gr.Row():
841
+ input_image = gr.Image(label="Input image (Ignored for Z-Image-Turbo)", type="filepath", visible=False)
842
+ image_strength = gr.Slider(label="Denoise Strength", info="Ignored for Z-Image-Turbo", minimum=0.1, maximum=1.0, step=0.01, value=0.75, visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
 
844
+ gr.HTML('<div class="info-box">๐Ÿ“ <b>Image Size</b> - Select aspect ratio for different platforms</div>')
 
 
 
 
 
 
845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
  with gr.Row():
847
+ aspect_ratio = gr.Dropdown(
848
+ choices=list(ASPECT_RATIOS.keys()),
849
+ value="1:1 (Instagram Square)",
850
+ label="๐Ÿ“ฑ Aspect Ratio (SNS Presets)",
851
+ info="Choose the best ratio for your target platform"
852
+ )
 
853
 
854
+ with gr.Row():
855
+ width = gr.Slider(label="Width", minimum=256, maximum=1536, step=64, value=1536)
856
+ height = gr.Slider(label="Height", minimum=256, maximum=1536, step=64, value=1536)
857
 
858
+ with gr.Column():
859
+ with gr.Row():
860
+ cfg_scale = gr.Slider(label="CFG Scale", info="Forced to 0.0 for Turbo", minimum=0, maximum=20, step=0.5, value=0.0, interactive=False)
861
+ steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=25)
862
+
863
+ with gr.Row():
864
+ randomize_seed = gr.Checkbox(True, label="Randomize seed")
865
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0, randomize=True)
866
+ lora_scale = gr.Slider(label="LoRA Scale", minimum=0, maximum=3, step=0.01, value=0.95)
867
+
868
+ # Connect aspect ratio dropdown to width/height sliders
869
+ aspect_ratio.change(
870
+ fn=update_size,
871
+ inputs=[aspect_ratio],
872
+ outputs=[width, height]
873
+ )
874
+
875
+ # Footer
876
+ gr.HTML("""
877
+ <div class="footer-comic">
878
+ <p style="font-family:'Bangers',cursive;font-size:1.5rem;letter-spacing:2px">๐ŸŽจ Z-IMAGE GEN/LORA ๐ŸŽจ</p>
879
+ <p>Powered by Z-Image Turbo + LoRA Adapters</p>
880
+ <p>โšก Fast Generation โ€ข ๐ŸŽญ Multiple Styles โ€ข ๐Ÿ–ผ๏ธ High Quality</p>
881
+ <p style="margin-top:10px"><a href="https://www.humangen.ai" target="_blank" style="color:#FACC15;text-decoration:none;font-weight:bold;">๐Ÿ  www.humangen.ai</a></p>
882
+ </div>
883
+ """)
884
+
885
+ gallery.select(
886
+ update_selection,
887
+ inputs=[width, height],
888
+ outputs=[prompt, selected_info, selected_index, aspect_ratio, width, height]
889
+ )
890
+ custom_lora.input(
891
+ add_custom_lora,
892
+ inputs=[custom_lora],
893
+ outputs=[custom_lora_info, custom_lora_button, gallery, selected_info, selected_index, prompt]
894
+ )
895
+ custom_lora_button.click(
896
+ remove_custom_lora,
897
+ outputs=[custom_lora_info, custom_lora_button, gallery, selected_info, selected_index, custom_lora]
898
+ )
899
+ gr.on(
900
+ triggers=[generate_button.click, prompt.submit],
901
+ fn=run_lora,
902
+ inputs=[prompt, input_image, image_strength, cfg_scale, steps, selected_index, randomize_seed, seed, width, height, lora_scale],
903
+ outputs=[result, seed, progress_bar]
904
+ )
905
+
906
+ demo.queue()
907
+ demo.launch(css=COMIC_CSS, ssr_mode=False, show_error=True)