SIGMitch commited on
Commit
a7f5c4c
Β·
verified Β·
1 Parent(s): c6711ad

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +452 -0
  2. requirements.txt +3 -0
app.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import json
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ import requests
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ import sqlite3
11
+
12
+ # Configuration
13
+ ENDPOINT = 'hunyuan.intl.tencentcloudapi.com'
14
+ SERVICE = 'hunyuan'
15
+ VERSION = '2023-09-01'
16
+ REGION = 'ap-singapore'
17
+
18
+ # Get credentials from environment variables
19
+ SECRET_ID = os.environ.get('TENCENT_SECRET_ID', '')
20
+ SECRET_KEY = os.environ.get('TENCENT_SECRET_KEY', '')
21
+ APP_PASSWORD = os.environ.get('APP_PASSWORD', 'hunyuan3d')
22
+
23
+ # Database setup
24
+ DB_PATH = 'generations.db'
25
+
26
+ def init_database():
27
+ """Initialize SQLite database for storing generations"""
28
+ conn = sqlite3.connect(DB_PATH)
29
+ cursor = conn.cursor()
30
+ cursor.execute('''
31
+ CREATE TABLE IF NOT EXISTS generations (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ job_id TEXT UNIQUE,
34
+ prompt TEXT,
35
+ status TEXT,
36
+ created_at TIMESTAMP,
37
+ completed_at TIMESTAMP,
38
+ result_files TEXT,
39
+ preview_image TEXT,
40
+ settings TEXT
41
+ )
42
+ ''')
43
+ conn.commit()
44
+ conn.close()
45
+
46
+ init_database()
47
+
48
+ def sha256(message):
49
+ """Calculate SHA256 hash"""
50
+ return hashlib.sha256(message.encode('utf-8')).hexdigest()
51
+
52
+ def hmac_sha256(key, message):
53
+ """Calculate HMAC-SHA256"""
54
+ if isinstance(key, str):
55
+ key = key.encode('utf-8')
56
+ return hmac.new(key, message.encode('utf-8'), hashlib.sha256).digest()
57
+
58
+ def hmac_sha256_hex(key, message):
59
+ """Calculate HMAC-SHA256 and return hex"""
60
+ if isinstance(key, str):
61
+ key = key.encode('utf-8')
62
+ return hmac.new(key, message.encode('utf-8'), hashlib.sha256).hexdigest()
63
+
64
+ def make_request(action, payload):
65
+ """Make authenticated request to Tencent Cloud API"""
66
+ timestamp = int(time.time())
67
+ date = datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d')
68
+ payload_str = json.dumps(payload)
69
+
70
+ # Build canonical request
71
+ http_request_method = 'POST'
72
+ canonical_uri = '/'
73
+ canonical_query_string = ''
74
+ canonical_headers = f'content-type:application/json; charset=utf-8\nhost:{ENDPOINT}\nx-tc-action:{action.lower()}\n'
75
+ signed_headers = 'content-type;host;x-tc-action'
76
+ hashed_request_payload = sha256(payload_str)
77
+ canonical_request = f'{http_request_method}\n{canonical_uri}\n{canonical_query_string}\n{canonical_headers}\n{signed_headers}\n{hashed_request_payload}'
78
+
79
+ # Build string to sign
80
+ algorithm = 'TC3-HMAC-SHA256'
81
+ hashed_canonical_request = sha256(canonical_request)
82
+ credential_scope = f'{date}/{SERVICE}/tc3_request'
83
+ string_to_sign = f'{algorithm}\n{timestamp}\n{credential_scope}\n{hashed_canonical_request}'
84
+
85
+ # Calculate signature
86
+ k_date = hmac_sha256(f'TC3{SECRET_KEY}', date)
87
+ k_service = hmac_sha256(k_date, SERVICE)
88
+ k_signing = hmac_sha256(k_service, 'tc3_request')
89
+ signature = hmac_sha256_hex(k_signing, string_to_sign)
90
+
91
+ # Build authorization header
92
+ authorization = f'{algorithm} Credential={SECRET_ID}/{credential_scope}, SignedHeaders={signed_headers}, Signature={signature}'
93
+
94
+ # Make request
95
+ headers = {
96
+ 'Content-Type': 'application/json; charset=utf-8',
97
+ 'Host': ENDPOINT,
98
+ 'X-TC-Action': action,
99
+ 'X-TC-Version': VERSION,
100
+ 'X-TC-Timestamp': str(timestamp),
101
+ 'X-TC-Region': REGION,
102
+ 'Authorization': authorization,
103
+ }
104
+
105
+ response = requests.post(f'https://{ENDPOINT}', headers=headers, data=payload_str)
106
+ return response.json()
107
+
108
+ def save_generation(job_id, prompt, settings):
109
+ """Save generation to database"""
110
+ conn = sqlite3.connect(DB_PATH)
111
+ cursor = conn.cursor()
112
+ cursor.execute('''
113
+ INSERT OR REPLACE INTO generations (job_id, prompt, status, created_at, settings)
114
+ VALUES (?, ?, ?, ?, ?)
115
+ ''', (job_id, prompt, 'WAIT', datetime.now(), json.dumps(settings)))
116
+ conn.commit()
117
+ conn.close()
118
+
119
+ def update_generation(job_id, status, result_files=None, preview_image=None):
120
+ """Update generation status in database"""
121
+ conn = sqlite3.connect(DB_PATH)
122
+ cursor = conn.cursor()
123
+ if status == 'DONE':
124
+ cursor.execute('''
125
+ UPDATE generations
126
+ SET status = ?, completed_at = ?, result_files = ?, preview_image = ?
127
+ WHERE job_id = ?
128
+ ''', (status, datetime.now(), json.dumps(result_files) if result_files else None, preview_image, job_id))
129
+ else:
130
+ cursor.execute('''
131
+ UPDATE generations
132
+ SET status = ?
133
+ WHERE job_id = ?
134
+ ''', (status, job_id))
135
+ conn.commit()
136
+ conn.close()
137
+
138
+ def get_all_generations():
139
+ """Get all generations from database"""
140
+ conn = sqlite3.connect(DB_PATH)
141
+ cursor = conn.cursor()
142
+ cursor.execute('''
143
+ SELECT job_id, prompt, status, created_at, completed_at, result_files, preview_image
144
+ FROM generations
145
+ ORDER BY created_at DESC
146
+ ''')
147
+ rows = cursor.fetchall()
148
+ conn.close()
149
+ return rows
150
+
151
+ def submit_generation(
152
+ prompt,
153
+ image,
154
+ image_url,
155
+ multi_left,
156
+ multi_right,
157
+ multi_back,
158
+ generate_type,
159
+ face_count,
160
+ polygon_type,
161
+ enable_pbr,
162
+ format_3d
163
+ ):
164
+ """Submit 3D generation job"""
165
+ if not SECRET_ID or not SECRET_KEY:
166
+ return "❌ Error: API credentials not configured. Please set TENCENT_SECRET_ID and TENCENT_SECRET_KEY environment variables."
167
+
168
+ # Build payload
169
+ payload = {}
170
+
171
+ # Text prompt or image
172
+ if prompt and prompt.strip():
173
+ payload['Prompt'] = prompt.strip()
174
+
175
+ if image is not None:
176
+ # Convert PIL image to base64
177
+ import base64
178
+ from io import BytesIO
179
+ buffered = BytesIO()
180
+ image.save(buffered, format="PNG")
181
+ img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
182
+ payload['ImageBase64'] = img_base64
183
+ elif image_url and image_url.strip():
184
+ payload['ImageUrl'] = image_url.strip()
185
+
186
+ # Multi-view images
187
+ multi_view_array = []
188
+ if multi_left and multi_left.strip():
189
+ multi_view_array.append({'ViewType': 'left', 'ViewImageUrl': multi_left.strip()})
190
+ if multi_right and multi_right.strip():
191
+ multi_view_array.append({'ViewType': 'right', 'ViewImageUrl': multi_right.strip()})
192
+ if multi_back and multi_back.strip():
193
+ multi_view_array.append({'ViewType': 'back', 'ViewImageUrl': multi_back.strip()})
194
+
195
+ if multi_view_array:
196
+ payload['MultiViewImages'] = multi_view_array
197
+
198
+ # Settings
199
+ payload['GenerateType'] = generate_type
200
+ payload['FaceCount'] = face_count
201
+ if generate_type == 'LowPoly':
202
+ payload['PolygonType'] = polygon_type
203
+ payload['EnablePBR'] = enable_pbr
204
+ payload['Format3D'] = format_3d
205
+
206
+ # Validate
207
+ if not payload.get('Prompt') and not payload.get('ImageUrl') and not payload.get('ImageBase64'):
208
+ return "❌ Error: Please provide either a text prompt or an image."
209
+
210
+ try:
211
+ # Submit job
212
+ response = make_request('SubmitHunyuanTo3DProJob', payload)
213
+
214
+ if 'Response' in response:
215
+ if 'Error' in response['Response']:
216
+ return f"❌ API Error: {response['Response']['Error']['Message']}"
217
+
218
+ job_id = response['Response'].get('JobId')
219
+ if job_id:
220
+ # Save to database
221
+ settings = {
222
+ 'generate_type': generate_type,
223
+ 'face_count': face_count,
224
+ 'polygon_type': polygon_type,
225
+ 'enable_pbr': enable_pbr,
226
+ 'format_3d': format_3d
227
+ }
228
+ save_generation(job_id, prompt or 'Image-to-3D', settings)
229
+
230
+ return f"βœ… Job submitted successfully!\n\n**Job ID:** {job_id}\n\nYour generation has been queued. Check the 'View Generations' tab to monitor progress."
231
+ else:
232
+ return "❌ Error: No Job ID returned from API"
233
+ else:
234
+ return f"❌ Error: Unexpected response format: {response}"
235
+
236
+ except Exception as e:
237
+ return f"❌ Error: {str(e)}"
238
+
239
+ def check_job_status(job_id):
240
+ """Check the status of a job"""
241
+ if not job_id or not job_id.strip():
242
+ return "⚠️ Please enter a Job ID"
243
+
244
+ try:
245
+ payload = {'JobId': job_id.strip()}
246
+ response = make_request('QueryHunyuanTo3DProJob', payload)
247
+
248
+ if 'Response' in response:
249
+ if 'Error' in response['Response']:
250
+ return f"❌ API Error: {response['Response']['Error']['Message']}"
251
+
252
+ status = response['Response'].get('Status', 'UNKNOWN')
253
+
254
+ # Update database
255
+ if status == 'DONE':
256
+ result_files = response['Response'].get('ResultFile3Ds', [])
257
+ preview_image = result_files[0].get('PreviewImageUrl') if result_files else None
258
+ update_generation(job_id, status, result_files, preview_image)
259
+ elif status in ['FAIL', 'WAIT', 'RUN']:
260
+ update_generation(job_id, status)
261
+
262
+ # Format response
263
+ result = f"**Job ID:** {job_id}\n**Status:** {status}\n\n"
264
+
265
+ if status == 'DONE':
266
+ result += "βœ… **Generation Complete!**\n\n"
267
+ result_files = response['Response'].get('ResultFile3Ds', [])
268
+ for i, file in enumerate(result_files):
269
+ result += f"**File {i+1}:**\n"
270
+ result += f"- Type: {file.get('Type', 'N/A')}\n"
271
+ result += f"- URL: {file.get('Url', 'N/A')}\n"
272
+ if file.get('PreviewImageUrl'):
273
+ result += f"- Preview: {file.get('PreviewImageUrl')}\n"
274
+ result += "\n"
275
+ elif status == 'FAIL':
276
+ error_msg = response['Response'].get('ErrorMessage', 'Unknown error')
277
+ result += f"❌ **Generation Failed**\nError: {error_msg}"
278
+ elif status == 'WAIT':
279
+ result += "⏳ **Waiting in queue...**"
280
+ elif status == 'RUN':
281
+ result += "🎨 **Generating your 3D model...**"
282
+
283
+ return result
284
+ else:
285
+ return f"❌ Error: Unexpected response format"
286
+
287
+ except Exception as e:
288
+ return f"❌ Error: {str(e)}"
289
+
290
+ def list_generations():
291
+ """List all generations as formatted text"""
292
+ generations = get_all_generations()
293
+
294
+ if not generations:
295
+ return "No generations yet. Start creating!"
296
+
297
+ result = f"# πŸ“¦ All Generations ({len(generations)} total)\n\n"
298
+
299
+ for gen in generations:
300
+ job_id, prompt, status, created_at, completed_at, result_files, preview_image = gen
301
+
302
+ status_emoji = {
303
+ 'DONE': 'βœ…',
304
+ 'FAIL': '❌',
305
+ 'WAIT': '⏳',
306
+ 'RUN': '🎨'
307
+ }.get(status, '❓')
308
+
309
+ result += f"## {status_emoji} {job_id}\n"
310
+ result += f"**Prompt:** {prompt}\n"
311
+ result += f"**Status:** {status}\n"
312
+ result += f"**Created:** {created_at}\n"
313
+
314
+ if completed_at:
315
+ result += f"**Completed:** {completed_at}\n"
316
+
317
+ if status == 'DONE' and result_files:
318
+ files = json.loads(result_files)
319
+ result += "\n**Files:**\n"
320
+ for file in files:
321
+ result += f"- [{file.get('Type')}]({file.get('Url')})\n"
322
+
323
+ result += "\n---\n\n"
324
+
325
+ return result
326
+
327
+ def auto_refresh_generations():
328
+ """Auto-refresh generations list"""
329
+ return list_generations()
330
+
331
+ # Custom CSS
332
+ custom_css = """
333
+ .gradio-container {
334
+ max-width: 1200px !important;
335
+ }
336
+ .status-box {
337
+ border-radius: 8px;
338
+ padding: 1rem;
339
+ margin: 1rem 0;
340
+ }
341
+ """
342
+
343
+ # Build Gradio Interface
344
+ with gr.Blocks(css=custom_css, title="Hunyuan 3D Studio", theme=gr.themes.Soft()) as app:
345
+ gr.Markdown("""
346
+ # 🎨 Hunyuan 3D Studio
347
+ ### Transform your imagination into stunning 3D models
348
+ Powered by Tencent Cloud Hunyuan 3D API
349
+ """)
350
+
351
+ with gr.Tabs():
352
+ # Tab 1: Generate
353
+ with gr.Tab("πŸš€ Generate"):
354
+ with gr.Row():
355
+ with gr.Column(scale=2):
356
+ prompt = gr.Textbox(
357
+ label="Text Prompt",
358
+ placeholder="Describe your 3D model... (e.g., A futuristic sci-fi spaceship with sleek curves and neon lights)",
359
+ lines=3
360
+ )
361
+
362
+ with gr.Accordion("πŸ“Έ Image Input", open=False):
363
+ image = gr.Image(label="Upload Image", type="pil")
364
+ image_url = gr.Textbox(label="Or Image URL", placeholder="https://example.com/image.jpg")
365
+ gr.Markdown("*Note: Either Prompt OR Image is required. Cannot use both together (except in Sketch mode).*")
366
+
367
+ with gr.Accordion("🎭 Multi-View Images (Optional)", open=False):
368
+ multi_left = gr.Textbox(label="Left View URL", placeholder="https://...")
369
+ multi_right = gr.Textbox(label="Right View URL", placeholder="https://...")
370
+ multi_back = gr.Textbox(label="Back View URL", placeholder="https://...")
371
+
372
+ with gr.Column(scale=1):
373
+ gr.Markdown("### βš™οΈ Settings")
374
+ generate_type = gr.Dropdown(
375
+ choices=["Normal", "LowPoly", "Geometry", "Sketch"],
376
+ value="Normal",
377
+ label="Generate Type"
378
+ )
379
+ face_count = gr.Slider(
380
+ minimum=40000,
381
+ maximum=1500000,
382
+ value=500000,
383
+ step=10000,
384
+ label="Face Count"
385
+ )
386
+ polygon_type = gr.Dropdown(
387
+ choices=["triangle", "quadrilateral"],
388
+ value="triangle",
389
+ label="Polygon Type (LowPoly only)"
390
+ )
391
+ enable_pbr = gr.Checkbox(label="Enable PBR Materials", value=False)
392
+ format_3d = gr.Dropdown(
393
+ choices=["GLB", "FBX", "OBJ", "USD", "GLTF"],
394
+ value="GLB",
395
+ label="Output Format"
396
+ )
397
+
398
+ submit_btn = gr.Button("✨ Generate 3D Model", variant="primary", size="lg")
399
+ output = gr.Markdown(label="Result")
400
+
401
+ submit_btn.click(
402
+ fn=submit_generation,
403
+ inputs=[
404
+ prompt, image, image_url,
405
+ multi_left, multi_right, multi_back,
406
+ generate_type, face_count, polygon_type, enable_pbr, format_3d
407
+ ],
408
+ outputs=output
409
+ )
410
+
411
+ # Tab 2: Check Status
412
+ with gr.Tab("πŸ” Check Status"):
413
+ gr.Markdown("### Check the status of a specific job")
414
+ with gr.Row():
415
+ job_id_input = gr.Textbox(label="Job ID", placeholder="Enter Job ID to check status")
416
+ check_btn = gr.Button("Check Status", variant="secondary")
417
+ status_output = gr.Markdown(label="Status")
418
+
419
+ check_btn.click(
420
+ fn=check_job_status,
421
+ inputs=job_id_input,
422
+ outputs=status_output
423
+ )
424
+
425
+ # Tab 3: View Generations
426
+ with gr.Tab("πŸ“¦ View Generations"):
427
+ gr.Markdown("### All your generations")
428
+ refresh_btn = gr.Button("πŸ”„ Refresh", variant="secondary")
429
+ generations_output = gr.Markdown(value=list_generations())
430
+
431
+ refresh_btn.click(
432
+ fn=auto_refresh_generations,
433
+ outputs=generations_output
434
+ )
435
+
436
+ gr.Markdown("""
437
+ ---
438
+ ### πŸ“ Notes:
439
+ - Generations typically take 2-5 minutes
440
+ - All generations are stored and can be viewed in the 'View Generations' tab
441
+ - You can run multiple generations simultaneously
442
+ - Download links are available once generation is complete
443
+ """)
444
+
445
+ # Launch with authentication
446
+ if __name__ == "__main__":
447
+ app.queue()
448
+ app.launch(
449
+ auth=("user", APP_PASSWORD),
450
+ auth_message="Enter password to access Hunyuan 3D Studio",
451
+ share=False
452
+ )
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio==4.44.0
2
+ requests==2.31.0
3
+ Pillow==10.3.0