Akhmad123 commited on
Commit
4ce91bb
·
verified ·
1 Parent(s): 95e967f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +178 -405
app.py CHANGED
@@ -1,451 +1,224 @@
1
  import gradio as gr
2
- from datetime import datetime
3
- from fpdf import FPDF
4
- import os
5
- import uuid
6
- import requests
7
  import replicate
 
 
8
  from groq import Groq
 
9
 
10
  # ============================
11
- # CONFIG
12
  # ============================
13
- REPLICATE_API_TOKEN = "r8_OHfjZvEbVoU9fuioi4qfQ50OHdpAmqV4OAa5b"
14
- os.environ["REPLICATE_API_TOKEN"] = REPLICATE_API_TOKEN
15
-
16
- GROQ_API_KEY = "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"
17
- client = Groq(api_key=GROQ_API_KEY)
18
-
19
- MAX_CHAPTERS = 10
20
- PANELS_PER_CHAPTER = 8 # sesuai permintaan
21
 
22
  # ============================
23
- # USER DB
24
  # ============================
25
- USER_DB = {
26
- "akhmad": {"password": "12345", "tier": "super"},
27
- "premium_user": {"password": "abcde", "tier": "premium"},
28
- "free_user": {"password": "00000", "tier": "free"},
29
- }
 
 
 
 
 
 
 
 
30
 
31
- # ============================
32
- # HELPERS
33
- # ============================
34
- def safe_text(text):
35
- if not text:
36
- return ""
37
- return ''.join(c if 32 <= ord(c) <= 126 else ' ' for c in str(text))
38
 
39
- def ensure_outputs_dir():
40
  os.makedirs("outputs", exist_ok=True)
 
 
41
 
42
- # ============================
43
- # AI TEXT GENERATOR (GROQ)
44
- # ============================
45
- def ai_generate(prompt):
46
- try:
47
- response = client.chat.completions.create(
48
- model="llama-3.3-70b-versatile",
49
- messages=[{"role": "user", "content": prompt}],
50
- max_tokens=800
51
- )
52
- return response.choices[0].message.content
53
- except Exception as e:
54
- return f"AI gagal menghasilkan teks. Error: {str(e)}"
55
 
56
- # ============================
57
- # PANEL GENERATOR
58
- # ============================
59
- def generate_comic_panels(goal, style, tone, chapter_title, panels=PANELS_PER_CHAPTER):
60
- prompt = f"""
61
- Kamu adalah penulis komik anak Islami berbahasa Indonesia.
62
-
63
- Buatkan {panels} panel komik untuk satu bab cerita.
64
- Judul bab: {chapter_title}
65
- Tema utama: {goal}
66
- Gaya: {style}
67
- Tone: {tone}
68
- Target: anak-anak SD, Islami, lembut, positif.
69
-
70
- Format output HARUS seperti ini:
71
-
72
- Panel 1: ...
73
- Panel 2: ...
74
- Panel 3: ...
75
- ...
76
- Panel {panels}: ...
77
-
78
- Jangan pakai bullet, jangan pakai heading.
79
- """
80
- raw = ai_generate(prompt)
81
- lines = [l.strip() for l in raw.split("\n") if l.strip()]
82
- panels_text = []
83
- for l in lines:
84
- if l.lower().startswith("panel"):
85
- panels_text.append(l)
86
- if not panels_text:
87
- panels_text = [f"Panel {i+1}: {raw}" for i in range(panels)]
88
- return panels_text[:panels]
89
 
90
  # ============================
91
- # VISUAL PROMPT GENERATOR
92
  # ============================
93
- def convert_panel_to_visual_prompt(panel_text, character_mode, character_desc):
94
- base_style = (
95
- "Cute pastel cartoon illustration, soft colors, round shapes, kid-friendly, "
96
- "Islamic-friendly, simple background, environmental setting, high quality."
 
97
  )
98
- if character_mode == "Custom" and character_desc.strip():
99
- return f"{base_style} Featuring: {character_desc}. Scene: {panel_text}."
100
- return f"{base_style} Scene: {panel_text}."
101
 
102
  # ============================
103
- # IMAGE MODEL ROUTER (16 MODEL)
104
  # ============================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- MODEL_MAP = {
107
- # Nama pendek -> ID model Replicate
108
- "Pastel-Mix": "fofr/pastel-mix",
109
- "SDXL": "stability-ai/sdxl",
110
- "Flux Schnell": "black-forest-labs/flux-schnell",
111
- "Flux Schnell Free": "black-forest-labs/flux-schnell-free",
112
- "Flux Pro": "black-forest-labs/flux-1.1-pro",
113
- "Flux Max": "black-forest-labs/flux-1.1-max",
114
- "Flux Dev": "black-forest-labs/flux-1.1-dev",
115
- "Flux Flex": "black-forest-labs/flux-1.1-flex",
116
- "Seedream 4.5": "bytedance/seedream-4.5",
117
- "Seedream 5 Lite": "bytedance/seedream-5-lite",
118
- "Ideogram Turbo": "ideogram-ai/ideogram-v3-turbo",
119
- "Ideogram Balanced": "ideogram-ai/ideogram-v3-balanced",
120
- "Ideogram Quality": "ideogram-ai/ideogram-v3-quality",
121
- "Recraft V4": "recraft-ai/recraft-v4",
122
- "Recraft V4 SVG": "recraft-ai/recraft-v4-svg",
123
- "Imagen Fast": "google/imagen-4-fast",
124
- }
125
-
126
- def generate_image_generic(model_id, prompt):
127
- try:
128
- out = replicate.run(model_id, input={"prompt": prompt})
129
- # Beberapa model mengembalikan list, beberapa dict
130
- if isinstance(out, list) and out:
131
- return out[0]
132
- if isinstance(out, dict):
133
- # coba beberapa kemungkinan key umum
134
- for key in ["output", "image", "images"]:
135
- if key in out and out[key]:
136
- if isinstance(out[key], list):
137
- return out[key][0]
138
- return out[key]
139
- except Exception:
140
- return ""
141
- return ""
142
-
143
- def generate_image_by_model(prompt, model_name):
144
- model_id = MODEL_MAP.get(model_name)
145
- if not model_id:
146
- # fallback ke Pastel-Mix
147
- model_id = MODEL_MAP["Pastel-Mix"]
148
- url = generate_image_generic(model_id, prompt)
149
- if not url:
150
- # fallback kedua: Pastel-Mix
151
- if model_name != "Pastel-Mix":
152
- url = generate_image_generic(MODEL_MAP["Pastel-Mix"], prompt)
153
- return url
154
-
155
- def download_image(url, filename):
156
- if not url:
157
- return ""
158
- ensure_outputs_dir()
159
- path = os.path.join("outputs", filename)
160
- try:
161
- r = requests.get(url, timeout=60)
162
- r.raise_for_status()
163
- with open(path, "wb") as f:
164
- f.write(r.content)
165
- return path
166
- except Exception:
167
- return ""
168
 
169
  # ============================
170
- # PDF BUILDER (COMIC)
171
  # ============================
172
- class ComicPDF(FPDF):
173
- def header(self):
174
- if self.page_no() == 1:
175
- return
176
- self.set_font("Helvetica", "I", 9)
177
- self.cell(0, 8, "AIPromptLab Comic", 0, 1, "R")
178
- self.ln(2)
179
-
180
- def footer(self):
181
- self.set_y(-15)
182
- self.set_font("Helvetica", "I", 9)
183
- self.cell(0, 10, f"Page {self.page_no()}", 0, 0, "C")
184
-
185
- def build_comic_pdf(username, goal, genre, tone, style, cover_brief,
186
- chapters_count, character_mode, character_desc,
187
- comic_name, image_model):
188
-
189
- ensure_outputs_dir()
190
- pdf = ComicPDF(format="A4")
191
- pdf.set_margins(15, 15, 15)
192
-
193
- # COVER
194
- cover_prompt = (
195
- f"Cute pastel cartoon cover, soft colors, kid-friendly, Islamic-friendly, "
196
- f"{cover_brief}, theme: {goal}"
197
- )
198
- cover_url = generate_image_by_model(cover_prompt, image_model)
199
- cover_path = download_image(cover_url, f"cover_{uuid.uuid4().hex}.png")
200
-
201
- pdf.add_page()
202
- pdf.set_auto_page_break(False)
203
 
204
- if cover_path:
205
- pdf.image(cover_path, x=0, y=0, w=210)
206
- pdf.set_y(210)
207
- else:
208
- pdf.set_fill_color(240, 230, 255)
209
- pdf.rect(0, 0, 210, 297, "F")
210
- pdf.set_y(50)
211
 
 
 
212
  pdf.set_auto_page_break(True, margin=15)
213
- pdf.set_y(max(pdf.get_y(), 50))
214
-
215
- pdf.set_font("Helvetica", "B", 22)
216
- pdf.multi_cell(180, 10, safe_text(goal), 0)
217
-
218
- pdf.set_font("Helvetica", "", 12)
219
- pdf.multi_cell(180, 7, safe_text(f"Genre: {genre} (Comic)"), 0)
220
- pdf.multi_cell(180, 7, safe_text(f"Tone: {tone} | Style: {style}"), 0)
221
 
222
- # TABLE OF CONTENTS
223
- pdf.add_page()
224
- pdf.set_font("Helvetica", "B", 18)
225
- pdf.cell(0, 10, "Daftar Isi (Komik)", 0, 1)
226
- pdf.ln(5)
227
 
228
- chapters_count = max(1, min(MAX_CHAPTERS, int(chapters_count)))
229
- chapters = [f"Bab {i}: Peristiwa Penting {i}" for i in range(1, chapters_count + 1)]
230
-
231
- pdf.set_font("Helvetica", "", 12)
232
- for i, ch in enumerate(chapters, start=1):
233
- pdf.cell(0, 8, safe_text(f"{i}. {ch}"), 0, 1)
234
-
235
- # CHAPTERS
236
- for idx, ch in enumerate(chapters, start=1):
237
  pdf.add_page()
238
  pdf.set_font("Helvetica", "B", 16)
239
- pdf.cell(0, 10, safe_text(ch), 0, 1)
240
- pdf.ln(4)
241
-
242
- panels_text = generate_comic_panels(goal, style, tone, ch, PANELS_PER_CHAPTER)
243
-
244
- for p_idx, panel in enumerate(panels_text, start=1):
245
- visual_prompt = convert_panel_to_visual_prompt(
246
- panel,
247
- character_mode,
248
- character_desc
249
- )
250
-
251
- illus_url = generate_image_by_model(visual_prompt, image_model)
252
- illus_path = download_image(
253
- illus_url,
254
- f"chapter_{idx}_panel_{p_idx}_{uuid.uuid4().hex}.png"
255
- )
256
 
 
257
  pdf.set_font("Helvetica", "B", 12)
258
- pdf.cell(0, 8, safe_text(f"Panel {p_idx}"), 0, 1)
259
- pdf.ln(1)
260
 
261
- if illus_path:
262
- y_start = pdf.get_y()
263
- pdf.image(illus_path, x=20, y=y_start, w=170)
264
- pdf.set_y(y_start + 80)
265
 
266
- pdf.set_font("Helvetica", "", 11)
267
- pdf.multi_cell(180, 6, safe_text(panel), 0)
268
- pdf.ln(3)
269
 
270
- if comic_name.strip():
271
- filename = os.path.join("outputs", f"{comic_name}.pdf")
272
- else:
273
- filename = os.path.join("outputs", "comic_output.pdf")
274
 
 
275
  pdf.output(filename)
276
  return filename
277
 
278
  # ============================
279
- # LOGIN
280
  # ============================
281
- def login(username, password):
282
- if username in USER_DB and USER_DB[username]["password"] == password:
283
- return True, USER_DB[username]["tier"], f"Login berhasil. Selamat datang, {username}!"
284
- return False, None, "❌ Username atau password salah."
285
 
286
- # ============================
287
- # UI
288
- # ============================
289
- with gr.Blocks() as demo:
290
-
291
- login_status = gr.State(False)
292
- login_user = gr.State("")
293
- login_tier = gr.State("free")
294
-
295
- # LOGIN UI
296
- with gr.Group(visible=True) as login_ui:
297
- gr.Markdown("# 🔐 Login untuk Menggunakan AIPromptLab")
298
- username = gr.Textbox(label="Username")
299
- password = gr.Textbox(label="Password", type="password")
300
- login_btn = gr.Button("Login")
301
- login_msg = gr.Markdown("")
302
-
303
- # MAIN UI
304
- with gr.Group(visible=False) as main_ui:
305
-
306
- gr.Markdown("# 🌟 AIPromptLab — Comic Generator 2.3 (Multi Model Image)")
307
-
308
- with gr.Tabs():
309
-
310
- with gr.Tab("Comic Generator"):
311
-
312
- comic_name = gr.Textbox(label="Nama Komik (tanpa .pdf)")
313
-
314
- comic_goal = gr.Textbox(label="Goal / Ide Cerita Komik")
315
-
316
- comic_genre = gr.Dropdown(
317
- label="Genre",
318
- choices=[
319
- "Islamic Children Comic",
320
- "Adventure",
321
- "Fantasy",
322
- "Moral Story",
323
- "Education"
324
- ],
325
- value="Islamic Children Comic"
326
- )
327
-
328
- comic_tone = gr.Dropdown(
329
- label="Tone",
330
- choices=[
331
- "Friendly",
332
- "Inspirational",
333
- "Educational",
334
- "Calm",
335
- "Humorous"
336
- ],
337
- value="Friendly"
338
- )
339
-
340
- comic_style = gr.Dropdown(
341
- label="Comic Style",
342
- choices=[
343
- "Cartoon Pastel",
344
- "Cute Cartoon",
345
- "Soft Pastel"
346
- ],
347
- value="Cartoon Pastel"
348
- )
349
-
350
- chapters_slider = gr.Slider(
351
- label="Jumlah Bab Komik",
352
- minimum=1,
353
- maximum=MAX_CHAPTERS,
354
- step=1,
355
- value=1
356
- )
357
-
358
- cover_brief = gr.Textbox(
359
- label="Cover Illustration Brief",
360
- value="Cute pastel cartoon cover of Islamic children adventure."
361
- )
362
-
363
- character_mode = gr.Dropdown(
364
- label="Mode Karakter",
365
- choices=["Otomatis", "Custom"],
366
- value="Otomatis"
367
- )
368
-
369
- character_desc = gr.Textbox(
370
- label="Deskripsi Karakter (jika Custom)",
371
- visible=False
372
- )
373
-
374
- def toggle_character_desc(mode):
375
- return gr.update(visible=(mode == "Custom"))
376
-
377
- character_mode.change(
378
- toggle_character_desc,
379
- [character_mode],
380
- [character_desc]
381
- )
382
-
383
- # IMAGE MODEL DROPDOWN (16 MODEL, A–Z)
384
- image_model = gr.Dropdown(
385
- label="Model Gambar",
386
- choices=sorted(list(MODEL_MAP.keys())),
387
- value="Pastel-Mix"
388
- )
389
-
390
- generate_pdf_btn = gr.Button("🚀 Generate Comic PDF")
391
- pdf_output = gr.File(label="Download Comic PDF")
392
-
393
- # LOGIN HANDLER
394
- def handle_login(username, password):
395
- ok, tier, msg = login(username, password)
396
- return (
397
- msg,
398
- gr.update(visible=not ok),
399
- gr.update(visible=ok),
400
- ok,
401
- username if ok else "",
402
- tier if ok else "free"
403
- )
404
-
405
- login_btn.click(
406
- handle_login,
407
- [username, password],
408
- [login_msg, login_ui, main_ui, login_status, login_user, login_tier]
409
- )
410
 
411
- # PDF GENERATOR HANDLER
412
- def handle_generate_pdf(login_status, login_user,
413
- comic_name, goal, genre, tone, style,
414
- chapters, cover_brief, character_mode,
415
- character_desc, image_model):
416
-
417
- if not login_status:
418
- return None
419
-
420
- pdf_path = build_comic_pdf(
421
- login_user,
422
- goal,
423
- genre,
424
- tone,
425
- style,
426
- cover_brief,
427
- chapters,
428
- character_mode,
429
- character_desc,
430
- comic_name,
431
- image_model
432
- )
433
- return pdf_path
434
-
435
- generate_pdf_btn.click(
436
- handle_generate_pdf,
437
- [
438
- login_status, login_user,
439
- comic_name, comic_goal, comic_genre, comic_tone,
440
- comic_style, chapters_slider, cover_brief,
441
- character_mode, character_desc, image_model
442
- ],
443
- pdf_output
444
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
- demo.launch(
447
- server_name="0.0.0.0",
448
- server_port=7860,
449
- theme=gr.themes.Soft(),
450
- ssr_mode=True
451
- )
 
1
  import gradio as gr
 
 
 
 
 
2
  import replicate
3
+ import uuid
4
+ import os
5
  from groq import Groq
6
+ from fpdf import FPDF
7
 
8
  # ============================
9
+ # CONFIG
10
  # ============================
11
+ os.environ["REPLICATE_API_TOKEN"] = "r8_Ap7YprjUOrZ7chAcXy2m1HAEDpFRbwV1dIM2Y" # GANTI DENGAN TOKENMU
12
+ client = Groq(api_key="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")
 
 
 
 
 
 
13
 
14
  # ============================
15
+ # NANO BANANA ENGINE
16
  # ============================
17
+ def nano_banana(prompt, aspect_ratio="1:1", resolution="1K"):
18
+ output = replicate.run(
19
+ "google/nano-banana-2",
20
+ input={
21
+ "prompt": prompt,
22
+ "resolution": resolution,
23
+ "image_input": [],
24
+ "aspect_ratio": aspect_ratio,
25
+ "image_search": False,
26
+ "google_search": False,
27
+ "output_format": "jpg"
28
+ }
29
+ )
30
 
31
+ if not hasattr(output, "url"):
32
+ return None, None
33
+
34
+ url = output.url
 
 
 
35
 
 
36
  os.makedirs("outputs", exist_ok=True)
37
+ filename = f"nano_{uuid.uuid4().hex}.jpg"
38
+ filepath = os.path.join("outputs", filename)
39
 
40
+ with open(filepath, "wb") as f:
41
+ f.write(output.read())
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ return url, filepath
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  # ============================
46
+ # GROQ TEXT GENERATOR
47
  # ============================
48
+ def ai(prompt):
49
+ res = client.chat.completions.create(
50
+ model="llama-3.3-70b-versatile",
51
+ messages=[{"role": "user", "content": prompt}],
52
+ max_tokens=800
53
  )
54
+ return res.choices[0].message.content
 
 
55
 
56
  # ============================
57
+ # COMIC PANEL GENERATOR
58
  # ============================
59
+ def generate_panels(story, style, chapters):
60
+ panels_per_chapter = 8
61
+ all_chapters = []
62
+
63
+ for c in range(1, chapters+1):
64
+ prompt = f"""
65
+ Buatkan {panels_per_chapter} panel komik.
66
+ Cerita: {story}
67
+ Style visual: {style}
68
+ Format:
69
+ Panel 1: ...
70
+ Panel 2: ...
71
+ ...
72
+ Panel {panels_per_chapter}: ...
73
+ """
74
+ raw = ai(prompt)
75
+ lines = [l for l in raw.split("\n") if l.strip().startswith("Panel")]
76
+ all_chapters.append(lines[:panels_per_chapter])
77
 
78
+ return all_chapters
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  # ============================
81
+ # VISUAL PROMPT BUILDER
82
  # ============================
83
+ def build_visual_prompt(panel_text, style):
84
+ return f"""
85
+ {style}, isometric 3D miniature diorama, soft pastel lighting,
86
+ cinematic depth, Nano Banana 2 optimized.
87
+ Scene: {panel_text}
88
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ # ============================
91
+ # PDF BUILDER
92
+ # ============================
93
+ class ComicPDF(FPDF):
94
+ pass
 
 
95
 
96
+ def build_pdf(story, style, chapters):
97
+ pdf = ComicPDF()
98
  pdf.set_auto_page_break(True, margin=15)
 
 
 
 
 
 
 
 
99
 
100
+ panels = generate_panels(story, style, chapters)
 
 
 
 
101
 
102
+ for c_idx, chapter in enumerate(panels, start=1):
 
 
 
 
 
 
 
 
103
  pdf.add_page()
104
  pdf.set_font("Helvetica", "B", 16)
105
+ pdf.cell(0, 10, f"Bab {c_idx}", 0, 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
+ for p_idx, panel in enumerate(chapter, start=1):
108
  pdf.set_font("Helvetica", "B", 12)
109
+ pdf.cell(0, 8, f"Panel {p_idx}", 0, 1)
 
110
 
111
+ visual_prompt = build_visual_prompt(panel, style)
112
+ url, path = nano_banana(visual_prompt)
 
 
113
 
114
+ if path:
115
+ pdf.image(path, x=20, w=170)
 
116
 
117
+ pdf.set_font("Helvetica", "", 11)
118
+ pdf.multi_cell(0, 6, panel)
119
+ pdf.ln(4)
 
120
 
121
+ filename = f"comic_{uuid.uuid4().hex}.pdf"
122
  pdf.output(filename)
123
  return filename
124
 
125
  # ============================
126
+ # GRADIO UI
127
  # ============================
128
+ with gr.Blocks() as app:
 
 
 
129
 
130
+ gr.Markdown("# 🌟 AIPromptLab 3.0 — Nano Banana Edition")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ with gr.Tabs():
133
+
134
+ # ============================
135
+ # TAB 1 — COMIC GENERATOR
136
+ # ============================
137
+ with gr.Tab("Comic Generator"):
138
+ story = gr.Textbox(label="Ide Cerita Komik")
139
+ style = gr.Dropdown(
140
+ label="Style Visual",
141
+ choices=[
142
+ "Pastel 3D Isometric",
143
+ "Cute 3D Cartoon",
144
+ "Realistic Cinematic",
145
+ "Lowpoly Diorama",
146
+ "Claymation 3D",
147
+ "Toy Photography",
148
+ "Anime 3D Soft Light"
149
+ ],
150
+ value="Pastel 3D Isometric"
151
+ )
152
+ chapters = gr.Slider(1, 10, value=1, step=1, label="Jumlah Bab")
153
+ btn = gr.Button("Generate Comic PDF")
154
+ out = gr.File()
155
+
156
+ btn.click(build_pdf, [story, style, chapters], out)
157
+
158
+ # ============================
159
+ # TAB 2 — IMAGE GENERATOR
160
+ # ============================
161
+ with gr.Tab("Image Generator"):
162
+ img_prompt = gr.Textbox(label="Prompt Gambar")
163
+ img_style = gr.Dropdown(
164
+ label="Style Visual",
165
+ choices=[
166
+ "Pastel 3D Isometric",
167
+ "Cute 3D Cartoon",
168
+ "Realistic Cinematic",
169
+ "Lowpoly Diorama",
170
+ "Claymation 3D",
171
+ "Toy Photography",
172
+ "Anime 3D Soft Light"
173
+ ],
174
+ value="Pastel 3D Isometric"
175
+ )
176
+ aspect = gr.Dropdown(["1:1", "16:9", "9:16"], label="Aspect Ratio", value="1:1")
177
+ res = gr.Dropdown(["1K", "2K"], label="Resolution", value="1K")
178
+ btn2 = gr.Button("Generate Image")
179
+ img_out = gr.Image()
180
+
181
+ def generate_image(p, s, a, r):
182
+ prompt = f"{s}, Nano Banana optimized. Scene: {p}"
183
+ url, path = nano_banana(prompt, a, r)
184
+ return path
185
+
186
+ btn2.click(generate_image, [img_prompt, img_style, aspect, res], img_out)
187
+
188
+ # ============================
189
+ # TAB 3 — VIDEO GENERATOR (STORYBOARD)
190
+ # ============================
191
+ with gr.Tab("Video Storyboard"):
192
+ vid_desc = gr.Textbox(label="Deskripsi Video")
193
+ vid_style = gr.Dropdown(
194
+ label="Style Visual",
195
+ choices=[
196
+ "Pastel 3D Isometric",
197
+ "Cute 3D Cartoon",
198
+ "Realistic Cinematic",
199
+ "Lowpoly Diorama",
200
+ "Claymation 3D",
201
+ "Toy Photography",
202
+ "Anime 3D Soft Light"
203
+ ],
204
+ value="Pastel 3D Isometric"
205
+ )
206
+ btn3 = gr.Button("Generate Storyboard Frames")
207
+ frame_gallery = gr.Gallery(label="Frames")
208
+
209
+ def generate_storyboard(desc, style):
210
+ prompt = f"Buatkan 8 scene untuk storyboard video. Deskripsi: {desc}"
211
+ scenes = ai(prompt).split("\n")
212
+ frames = []
213
+
214
+ for s in scenes[:8]:
215
+ visual = build_visual_prompt(s, style)
216
+ url, path = nano_banana(visual)
217
+ if path:
218
+ frames.append(path)
219
+
220
+ return frames
221
+
222
+ btn3.click(generate_storyboard, [vid_desc, vid_style], frame_gallery)
223
 
224
+ app.launch()