Spaces:
Sleeping
Sleeping
v9: Gemini 3 Pro Image primary + 3.1 Flash + 2.5 Flash fallback, improved prompts
Browse files
app.py
CHANGED
|
@@ -7,7 +7,9 @@ GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY', '')
|
|
| 7 |
HF_TOKEN = os.environ.get('HF_TOKEN', '')
|
| 8 |
|
| 9 |
GEMINI_TEXT_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'
|
| 10 |
-
GEMINI_IMAGE_URL
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# ββ FONTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
_B = ['/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
|
|
@@ -37,9 +39,8 @@ def gemini_text(prompt, api_key):
|
|
| 37 |
return data['candidates'][0]['content']['parts'][0]['text']
|
| 38 |
|
| 39 |
# ββ GEMINI IMAGEN (page generation) ββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
-
def
|
| 41 |
-
"""Gemini
|
| 42 |
-
url = GEMINI_IMAGE_URL + '?key=' + api_key
|
| 43 |
body = {
|
| 44 |
'contents': [{'parts': [{'text': prompt}]}],
|
| 45 |
'generationConfig': {
|
|
@@ -47,9 +48,10 @@ def gemini_imagen(prompt, api_key, width=1024, height=1408):
|
|
| 47 |
'temperature': 1.0
|
| 48 |
}
|
| 49 |
}
|
| 50 |
-
|
|
|
|
| 51 |
try:
|
| 52 |
-
r = requests.post(
|
| 53 |
if r.status_code == 200:
|
| 54 |
data = r.json()
|
| 55 |
candidates = data.get('candidates', [])
|
|
@@ -60,17 +62,41 @@ def gemini_imagen(prompt, api_key, width=1024, height=1408):
|
|
| 60 |
img_bytes = base64.b64decode(part['inlineData']['data'])
|
| 61 |
img = Image.open(BytesIO(img_bytes)).convert('RGB')
|
| 62 |
return img.resize((width, height), Image.LANCZOS)
|
|
|
|
|
|
|
| 63 |
elif r.status_code == 429:
|
| 64 |
-
print('Gemini image 429, waiting ' + str(
|
| 65 |
-
time.sleep(
|
| 66 |
else:
|
| 67 |
-
print('Gemini image error ' + str(r.status_code) + ': ' + r.text[:
|
| 68 |
return None
|
| 69 |
except Exception as e:
|
| 70 |
print('Gemini image exception: ' + str(e)[:100])
|
| 71 |
return None
|
| 72 |
return None
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
# ββ POLLINATIONS FALLBACK βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 75 |
def pollinations(prompt, seed, retries=3):
|
| 76 |
enc = urllib.parse.quote(prompt[:600])
|
|
@@ -182,33 +208,66 @@ def fallback_script(topic, num_pages, style, language):
|
|
| 182 |
|
| 183 |
# ββ PAGE IMAGE PROMPT BUILDER βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 184 |
def build_page_prompt(page_data, script, style):
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
prompt = (
|
| 202 |
-
|
| 203 |
-
'
|
| 204 |
-
'
|
| 205 |
-
+
|
| 206 |
-
'
|
| 207 |
-
'Bubbles
|
| 208 |
-
'
|
| 209 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
)
|
| 211 |
-
return prompt[:
|
| 212 |
|
| 213 |
# ββ GENERATE ONE PAGE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 214 |
def generate_page(page_data, script, style, title, page_num, total, api_key):
|
|
@@ -217,7 +276,7 @@ def generate_page(page_data, script, style, title, page_num, total, api_key):
|
|
| 217 |
|
| 218 |
print('Page ' + str(page_num) + '/' + str(total) + ' generating...')
|
| 219 |
|
| 220 |
-
# Try Gemini
|
| 221 |
art = None
|
| 222 |
if api_key:
|
| 223 |
art = gemini_imagen(prompt, api_key)
|
|
@@ -275,7 +334,7 @@ def generate_comic_ui(topic, num_pages, style, language, gemini_key):
|
|
| 275 |
script, pages = generate_comic(topic, num_pages, style, language, gemini_key)
|
| 276 |
info = json.dumps({'title':script.get('title',''),'summary':script.get('story_summary',''),
|
| 277 |
'characters':script.get('characters',[]),'total_pages':len(pages),
|
| 278 |
-
'engine':'Gemini
|
| 279 |
ensure_ascii=False, indent=2)
|
| 280 |
return script.get('title',topic), script.get('story_summary',''), pages, info
|
| 281 |
|
|
@@ -284,7 +343,7 @@ STYLES=['manga','superhero comic','watercolor comic','noir comic','cartoon',
|
|
| 284 |
|
| 285 |
with gr.Blocks(title='Comic AI Generator') as demo:
|
| 286 |
gr.Markdown('# Comic AI Generator')
|
| 287 |
-
gr.Markdown('Powered by **Gemini
|
| 288 |
with gr.Tabs():
|
| 289 |
with gr.Tab('Generate Comic'):
|
| 290 |
with gr.Row():
|
|
|
|
| 7 |
HF_TOKEN = os.environ.get('HF_TOKEN', '')
|
| 8 |
|
| 9 |
GEMINI_TEXT_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'
|
| 10 |
+
GEMINI_IMAGE_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent'
|
| 11 |
+
GEMINI_IMAGE_URL2 = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent'
|
| 12 |
+
GEMINI_IMAGE_URL3 = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent'
|
| 13 |
|
| 14 |
# ββ FONTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
_B = ['/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
|
|
|
|
| 39 |
return data['candidates'][0]['content']['parts'][0]['text']
|
| 40 |
|
| 41 |
# ββ GEMINI IMAGEN (page generation) ββββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
+
def gemini_imagen_call(url, prompt, api_key, width, height):
|
| 43 |
+
"""Single Gemini image model call with retry on 429."""
|
|
|
|
| 44 |
body = {
|
| 45 |
'contents': [{'parts': [{'text': prompt}]}],
|
| 46 |
'generationConfig': {
|
|
|
|
| 48 |
'temperature': 1.0
|
| 49 |
}
|
| 50 |
}
|
| 51 |
+
full_url = url + '?key=' + api_key
|
| 52 |
+
for attempt in range(2):
|
| 53 |
try:
|
| 54 |
+
r = requests.post(full_url, json=body, timeout=120)
|
| 55 |
if r.status_code == 200:
|
| 56 |
data = r.json()
|
| 57 |
candidates = data.get('candidates', [])
|
|
|
|
| 62 |
img_bytes = base64.b64decode(part['inlineData']['data'])
|
| 63 |
img = Image.open(BytesIO(img_bytes)).convert('RGB')
|
| 64 |
return img.resize((width, height), Image.LANCZOS)
|
| 65 |
+
print('Gemini image: no image in response from ' + url.split('/models/')[1].split(':')[0])
|
| 66 |
+
return None
|
| 67 |
elif r.status_code == 429:
|
| 68 |
+
print('Gemini image 429 (' + url.split('/models/')[1].split(':')[0] + '), waiting ' + str(20*(attempt+1)) + 's...')
|
| 69 |
+
time.sleep(20 * (attempt + 1))
|
| 70 |
else:
|
| 71 |
+
print('Gemini image error ' + str(r.status_code) + ' (' + url.split('/models/')[1].split(':')[0] + '): ' + r.text[:200])
|
| 72 |
return None
|
| 73 |
except Exception as e:
|
| 74 |
print('Gemini image exception: ' + str(e)[:100])
|
| 75 |
return None
|
| 76 |
return None
|
| 77 |
|
| 78 |
+
def gemini_imagen(prompt, api_key, width=1024, height=1408):
|
| 79 |
+
"""Try Gemini image models from best to fastest: 3-pro β 3.1-flash β 2.5-flash."""
|
| 80 |
+
# Model 1: Gemini 3 Pro Image (best quality)
|
| 81 |
+
print(' Trying Gemini 3 Pro Image...')
|
| 82 |
+
img = gemini_imagen_call(GEMINI_IMAGE_URL, prompt, api_key, width, height)
|
| 83 |
+
if img:
|
| 84 |
+
print(' Gemini 3 Pro Image SUCCESS')
|
| 85 |
+
return img
|
| 86 |
+
# Model 2: Gemini 3.1 Flash Image (fast + good quality)
|
| 87 |
+
print(' Trying Gemini 3.1 Flash Image...')
|
| 88 |
+
img = gemini_imagen_call(GEMINI_IMAGE_URL2, prompt, api_key, width, height)
|
| 89 |
+
if img:
|
| 90 |
+
print(' Gemini 3.1 Flash Image SUCCESS')
|
| 91 |
+
return img
|
| 92 |
+
# Model 3: Gemini 2.5 Flash Image (fastest)
|
| 93 |
+
print(' Trying Gemini 2.5 Flash Image...')
|
| 94 |
+
img = gemini_imagen_call(GEMINI_IMAGE_URL3, prompt, api_key, width, height)
|
| 95 |
+
if img:
|
| 96 |
+
print(' Gemini 2.5 Flash Image SUCCESS')
|
| 97 |
+
return img
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
# ββ POLLINATIONS FALLBACK βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 101 |
def pollinations(prompt, seed, retries=3):
|
| 102 |
enc = urllib.parse.quote(prompt[:600])
|
|
|
|
| 208 |
|
| 209 |
# ββ PAGE IMAGE PROMPT BUILDER βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 210 |
def build_page_prompt(page_data, script, style):
|
| 211 |
+
"""Build a rich, detailed prompt for Gemini 3 Pro image generation."""
|
| 212 |
+
title = script.get('title', 'Comic')
|
| 213 |
+
genre = script.get('genre', 'adventure')
|
| 214 |
+
chars = script.get('characters', [])
|
| 215 |
+
panels = page_data.get('panels', [])[:4]
|
| 216 |
+
page_num = page_data.get('page_number', 1)
|
| 217 |
+
total_p = script.get('total_pages', 1)
|
| 218 |
+
|
| 219 |
+
# Character descriptions for consistency
|
| 220 |
+
char_descs = []
|
| 221 |
+
for c in chars[:3]:
|
| 222 |
+
char_descs.append(c['name'] + ': ' + c.get('description','') + (', wears ' + c.get('costume','') if c.get('costume') else ''))
|
| 223 |
+
char_ref = '. '.join(char_descs) if char_descs else ''
|
| 224 |
+
|
| 225 |
+
# Build per-panel descriptions with full dialogue
|
| 226 |
+
panel_parts = []
|
| 227 |
+
num_panels = len(panels)
|
| 228 |
+
for i, p in enumerate(panels):
|
| 229 |
+
scene = str(p.get('scene', ''))
|
| 230 |
+
action = str(p.get('action', ''))
|
| 231 |
+
caption = str(p.get('caption', ''))
|
| 232 |
+
dialogues = p.get('dialogue', [])
|
| 233 |
+
dlg_list = []
|
| 234 |
+
for d in dialogues:
|
| 235 |
+
dlg_list.append(d['character'] + ' says: "' + d['text'] + '"')
|
| 236 |
+
dlg_str = '; '.join(dlg_list) if dlg_list else 'no dialogue'
|
| 237 |
+
cap_str = (' Narrator caption box: "' + caption + '"') if caption else ''
|
| 238 |
+
panel_parts.append(
|
| 239 |
+
'PANEL ' + str(i+1) + ': Scene=' + scene +
|
| 240 |
+
'. Action=' + action + '. ' + dlg_str + cap_str
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# Choose layout based on panel count
|
| 244 |
+
if num_panels == 1:
|
| 245 |
+
layout_desc = 'ONE large full-page panel'
|
| 246 |
+
elif num_panels == 2:
|
| 247 |
+
layout_desc = 'TWO panels: large top panel, large bottom panel, separated by thick black gutter'
|
| 248 |
+
elif num_panels == 3:
|
| 249 |
+
layout_desc = 'THREE panels: wide panoramic top panel, two equal panels side-by-side at bottom, separated by thick black gutters'
|
| 250 |
+
else:
|
| 251 |
+
layout_desc = 'FOUR panels: large wide panel at top, two medium square panels in middle row, one wide panel at bottom, all separated by thick black gutters (8px)'
|
| 252 |
+
|
| 253 |
+
panels_text = ' | '.join(panel_parts)
|
| 254 |
|
| 255 |
prompt = (
|
| 256 |
+
'CREATE A PROFESSIONAL ' + style.upper() + ' COMIC BOOK PAGE. '
|
| 257 |
+
'Comic: "' + title + '" (' + genre + '). Page ' + str(page_num) + ' of ' + str(total_p) + '. '
|
| 258 |
+
'LAYOUT: ' + layout_desc + '. '
|
| 259 |
+
'PANELS: ' + panels_text + '. '
|
| 260 |
+
'SPEECH BUBBLES: White rounded oval bubbles with clear BLACK TEXT for every dialogue line. '
|
| 261 |
+
'Bubbles sized to fit text (max 25% of panel area), tail pointing to speaker. '
|
| 262 |
+
'Text must be legible. Do NOT overlap speech bubbles with main action. '
|
| 263 |
+
'STYLE REQUIREMENTS: '
|
| 264 |
+
+ style + ' art style. Bold black panel borders (8-10px). White gutter between panels. '
|
| 265 |
+
'Dynamic compositions. Dramatic lighting and shadows. Vivid expressive colors. '
|
| 266 |
+
'Professional comic book quality. NO watermarks, NO logos, NO page numbers in image. '
|
| 267 |
+
+ (('CHARACTERS (keep consistent): ' + char_ref + '. ') if char_ref else '') +
|
| 268 |
+
'Make every panel visually compelling and clearly readable as a comic book.'
|
| 269 |
)
|
| 270 |
+
return prompt[:1800]
|
| 271 |
|
| 272 |
# ββ GENERATE ONE PAGE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 273 |
def generate_page(page_data, script, style, title, page_num, total, api_key):
|
|
|
|
| 276 |
|
| 277 |
print('Page ' + str(page_num) + '/' + str(total) + ' generating...')
|
| 278 |
|
| 279 |
+
# Try Gemini image models (3-pro β 3.1-flash β 2.5-flash β Pollinations)
|
| 280 |
art = None
|
| 281 |
if api_key:
|
| 282 |
art = gemini_imagen(prompt, api_key)
|
|
|
|
| 334 |
script, pages = generate_comic(topic, num_pages, style, language, gemini_key)
|
| 335 |
info = json.dumps({'title':script.get('title',''),'summary':script.get('story_summary',''),
|
| 336 |
'characters':script.get('characters',[]),'total_pages':len(pages),
|
| 337 |
+
'engine':'Gemini 3 Pro Image β 3.1 Flash β 2.5 Flash β Pollinations fallback'},
|
| 338 |
ensure_ascii=False, indent=2)
|
| 339 |
return script.get('title',topic), script.get('story_summary',''), pages, info
|
| 340 |
|
|
|
|
| 343 |
|
| 344 |
with gr.Blocks(title='Comic AI Generator') as demo:
|
| 345 |
gr.Markdown('# Comic AI Generator')
|
| 346 |
+
gr.Markdown('Powered by **Gemini 3 Pro Image** β each page is one AI-generated full comic page (1240Γ1754 px) with panels, borders and speech bubbles drawn by AI.')
|
| 347 |
with gr.Tabs():
|
| 348 |
with gr.Tab('Generate Comic'):
|
| 349 |
with gr.Row():
|