tchung1970 Claude Opus 4.5 commited on
Commit
1182d82
·
1 Parent(s): 9783857

Redesign UI to match Z-Image-Turbo 2K dark theme

Browse files

- Two-column layout: fixed input panel (550px) + flexible output area
- Dark theme with black background and dark gray panels
- Replace width/height sliders with aspect ratio dropdown
- Add Apple-style CSS with blue accent color and rounded corners
- Simplified generate_image function with random seed
- Add CLAUDE.md documentation for codebase guidance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (2) hide show
  1. CLAUDE.md +70 -0
  2. app.py +538 -145
CLAUDE.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ LongCat-Image is a text-to-image generation model built on diffusion transformers, deployed as a Hugging Face Space with a Gradio interface. The model is based on the Flux architecture and supports both text-to-image generation and image editing.
8
+
9
+ ## Running the Application
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ pip install -r requirements.txt
14
+
15
+ # Run the Gradio app locally
16
+ python app.py
17
+ ```
18
+
19
+ The app launches with MCP server enabled on the default Gradio port.
20
+
21
+ ## Architecture
22
+
23
+ ### Core Components
24
+
25
+ **Transformer Model** (`longcat_image/models/longcat_image_dit.py`):
26
+ - `LongCatImageTransformer2DModel`: DiT-based transformer using Flux architecture
27
+ - Uses `FluxTransformerBlock` (19 layers) and `FluxSingleTransformerBlock` (38 layers)
28
+ - Supports gradient checkpointing for memory efficiency
29
+ - Position embeddings via `FluxPosEmbed` with RoPE
30
+
31
+ **Pipelines** (`longcat_image/pipelines/`):
32
+ - `LongCatImagePipeline`: Text-to-image generation with optional prompt rewriting
33
+ - `LongCatImageEditPipeline`: Image editing with vision-language conditioning
34
+ - Both pipelines inherit from `DiffusionPipeline` and support LoRA, CFG renorm, and VAE tiling/slicing
35
+
36
+ **Text Encoding**:
37
+ - Uses Qwen-based text encoder with chat template formatting
38
+ - Prompt template wraps user input between `<|im_start|>` and `<|im_end|>` tokens
39
+ - Maximum token length: 512
40
+
41
+ ### Key Configuration
42
+
43
+ - VAE scale factor: 8 (with 2x2 patch packing, effective 16x)
44
+ - Default sample size: 128 (1024px at 8x scale)
45
+ - Latent channels: 16
46
+ - Image dimensions must be divisible by 32
47
+
48
+ ### Prompt Rewriting
49
+
50
+ The pipeline includes built-in prompt engineering via `rewire_prompt()` that uses the text encoder to expand simple prompts into detailed descriptions. This can be disabled with `enable_prompt_rewrite=False`.
51
+
52
+ External prompt polishing is also available via `utils/prompt_utils.py` using Hugging Face Inference API (requires `HF_TOKEN`).
53
+
54
+ ## Model Loading
55
+
56
+ ```python
57
+ from longcat_image.models import LongCatImageTransformer2DModel
58
+ from longcat_image.pipelines import LongCatImagePipeline
59
+
60
+ MODEL_REPO = "meituan-longcat/LongCat-Image"
61
+
62
+ transformer = LongCatImageTransformer2DModel.from_pretrained(
63
+ MODEL_REPO, subfolder='transformer', torch_dtype=torch.bfloat16
64
+ )
65
+ pipe = LongCatImagePipeline.from_pretrained(MODEL_REPO, transformer=transformer)
66
+ ```
67
+
68
+ ## Environment Variables
69
+
70
+ - `HF_TOKEN`: Required for prompt polishing via external API
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import gradio as gr
2
  import numpy as np
3
- import os, random, json, spaces, torch, time, subprocess
4
 
5
  import torch
6
  from transformers import AutoProcessor, AutoTokenizer
@@ -11,24 +11,18 @@ from utils.image_utils import rescale_image
11
  from utils.prompt_utils import polish_prompt
12
 
13
 
14
- # GIT_DIR = "LongCat-Image"
15
- # GIT_URL = "https://github.com/yourusername/LongCat-Image.git"
16
-
17
- # if not os.path.isdir(GIT_DIR):
18
- # subprocess.run(["git", "clone", GIT_URL])
19
- # else:
20
- # print("Folder already exists.")
21
  MODEL_REPO = "meituan-longcat/LongCat-Image"
22
  MAX_SEED = np.iinfo(np.int32).max
23
 
24
- text_processor = AutoTokenizer.from_pretrained(
25
- MODEL_REPO,
 
26
  subfolder = 'tokenizer'
27
  )
28
- transformer = LongCatImageTransformer2DModel.from_pretrained(
29
- MODEL_REPO ,
30
- subfolder = 'transformer',
31
- torch_dtype=torch.bfloat16,
32
  use_safetensors=True
33
  ).to("cuda")
34
 
@@ -38,156 +32,555 @@ pipe = LongCatImagePipeline.from_pretrained(
38
  text_processor=text_processor
39
  )
40
  pipe.to("cuda", torch.bfloat16)
 
41
 
42
- def prepare(prompt, is_polish_prompt):
43
- if not is_polish_prompt: return prompt, False
44
- polished_prompt = polish_prompt(prompt)
45
- return polished_prompt, True
46
 
47
  @spaces.GPU
48
- def inference(
49
  prompt,
50
- negative_prompt="blurry ugly bad",
51
- width=1024,
52
- height=1024,
53
- seed=42,
54
- randomize_seed=True,
55
- guidance_scale=1.5,
56
- num_inference_steps=8,
57
  progress=gr.Progress(track_tqdm=True),
58
  ):
59
- timestamp = time.time()
60
- print(f"timestamp: {timestamp}")
 
 
 
 
 
 
 
 
 
 
 
 
61
 
 
62
 
63
- # generation
64
- if randomize_seed: seed = random.randint(0, MAX_SEED)
65
  generator = torch.Generator().manual_seed(seed)
66
 
67
- image = pipe(
68
- prompt= prompt,
69
- negative_prompt = negative_prompt,
70
- width=width,
71
- height=height,
72
- generator=generator,
73
- guidance_scale=guidance_scale,
74
- num_inference_steps=num_inference_steps,
75
- enable_prompt_rewrite= False
76
- ).images[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- return image, seed
 
 
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- def read_file(path: str) -> str:
82
- with open(path, 'r', encoding='utf-8') as f:
83
- content = f.read()
84
- return content
85
 
 
 
 
 
 
86
 
87
- css = """
88
- #col-container {
89
- margin: 0 auto;
90
- max-width: 960px;
91
  }
92
  """
93
 
94
- with open('static/data.json', 'r') as file:
95
- data = json.load(file)
96
- examples = data['examples']
97
-
98
- with gr.Blocks() as demo:
99
- with gr.Column(elem_id="col-container"):
100
- with gr.Column():
101
- gr.HTML(read_file("static/header.html"))
102
- with gr.Row():
103
- with gr.Column():
104
- prompt = gr.Textbox(
105
- label="Prompt",
106
- show_label=False,
107
- lines=2,
108
- placeholder="Enter your prompt",
109
- # container=False,
110
- )
111
- is_polish_prompt = gr.Checkbox(label="Polish prompt", value=False)
112
- run_button = gr.Button("Generate", variant="primary")
113
- with gr.Accordion("Advanced Settings", open=False):
114
-
115
- negative_prompt = gr.Textbox(
116
- label="Negative prompt",
117
- lines=2,
118
- container=False,
119
- placeholder="Enter your negative prompt",
120
- value="blurry ugly bad"
121
- )
122
- num_inference_steps = gr.Slider(
123
- label="Steps",
124
- minimum=1,
125
- maximum=50,
126
- step=1,
127
- value=20,
128
- )
129
- with gr.Row():
130
- width = gr.Slider(
131
- label="Width",
132
- minimum=512,
133
- maximum=1280,
134
- step=32,
135
- value=768,
136
- )
137
-
138
- height = gr.Slider(
139
- label="Height",
140
- minimum=512,
141
- maximum=1280,
142
- step=32,
143
- value=1024,
144
- )
145
- with gr.Row():
146
- seed = gr.Slider(
147
- label="Seed",
148
- minimum=0,
149
- maximum=MAX_SEED,
150
- step=1,
151
- value=42,
152
- )
153
- guidance_scale = gr.Slider(
154
- label="Guidance scale",
155
- minimum=0.0,
156
- maximum=10.0,
157
- step=0.1,
158
- value=1.0,
159
- )
160
-
161
-
162
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
163
-
164
- with gr.Column():
165
- output_image = gr.Image(label="Generated image", show_label=False)
166
- polished_prompt = gr.Textbox(label="Final prompt",lines=2, interactive=False)
167
-
168
-
169
- gr.Examples(examples=examples, inputs=[prompt])
170
- # gr.Markdown(read_file("static/footer.md"))
171
-
172
- run_button.click(
173
- fn=prepare,
174
- inputs=[prompt, is_polish_prompt],
175
- outputs=[polished_prompt, is_polish_prompt]
176
- ).then(
177
- fn=inference,
178
- inputs=[
179
- polished_prompt,
180
- negative_prompt,
181
- width,
182
- height,
183
- seed,
184
- randomize_seed,
185
- guidance_scale,
186
- num_inference_steps,
187
- ],
188
- outputs=[output_image, seed],
189
  )
190
 
 
 
 
191
 
192
  if __name__ == "__main__":
193
- demo.launch(mcp_server=True, css=css)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import numpy as np
3
+ import os, random, json, spaces, torch, time
4
 
5
  import torch
6
  from transformers import AutoProcessor, AutoTokenizer
 
11
  from utils.prompt_utils import polish_prompt
12
 
13
 
 
 
 
 
 
 
 
14
  MODEL_REPO = "meituan-longcat/LongCat-Image"
15
  MAX_SEED = np.iinfo(np.int32).max
16
 
17
+ print("Loading LongCat-Image model...")
18
+ text_processor = AutoTokenizer.from_pretrained(
19
+ MODEL_REPO,
20
  subfolder = 'tokenizer'
21
  )
22
+ transformer = LongCatImageTransformer2DModel.from_pretrained(
23
+ MODEL_REPO ,
24
+ subfolder = 'transformer',
25
+ torch_dtype=torch.bfloat16,
26
  use_safetensors=True
27
  ).to("cuda")
28
 
 
32
  text_processor=text_processor
33
  )
34
  pipe.to("cuda", torch.bfloat16)
35
+ print("Model loaded successfully!")
36
 
 
 
 
 
37
 
38
  @spaces.GPU
39
+ def generate_image(
40
  prompt,
41
+ aspect_ratio,
 
 
 
 
 
 
42
  progress=gr.Progress(track_tqdm=True),
43
  ):
44
+ """Generate an image using LongCat-Image model."""
45
+ if not prompt.strip():
46
+ raise gr.Error("Please enter a prompt to generate an image.")
47
+
48
+ # Parse aspect ratio to get dimensions
49
+ aspect_ratio_map = {
50
+ "1:1 (1024x1024)": (1024, 1024),
51
+ "4:3 (1024x768)": (1024, 768),
52
+ "3:4 (768x1024)": (768, 1024),
53
+ "16:9 (1024x576)": (1024, 576),
54
+ "9:16 (576x1024)": (576, 1024),
55
+ "3:2 (1024x672)": (1024, 672),
56
+ "2:3 (672x1024)": (672, 1024),
57
+ }
58
 
59
+ width, height = aspect_ratio_map.get(aspect_ratio, (1024, 1024))
60
 
61
+ # Generate random seed
62
+ seed = random.randint(0, MAX_SEED)
63
  generator = torch.Generator().manual_seed(seed)
64
 
65
+ progress(0.1, desc="Generating image...")
66
+
67
+ try:
68
+ image = pipe(
69
+ prompt=prompt,
70
+ negative_prompt="blurry ugly bad",
71
+ width=width,
72
+ height=height,
73
+ generator=generator,
74
+ guidance_scale=1.5,
75
+ num_inference_steps=20,
76
+ enable_prompt_rewrite=False
77
+ ).images[0]
78
+
79
+ progress(1.0, desc="Complete!")
80
+ return image
81
+
82
+ except Exception as e:
83
+ raise gr.Error(f"Generation failed: {str(e)}")
84
+
85
+
86
+ # Apple-style CSS for dark theme
87
+ apple_css = """
88
+ /* Global Styles */
89
+ .gradio-container {
90
+ max-width: 85vw !important;
91
+ margin: 0 auto !important;
92
+ padding: 48px 20px !important;
93
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Roboto', sans-serif !important;
94
+ }
95
+
96
+ /* Disable all transitions globally to prevent layout shifts */
97
+ * {
98
+ transition: none !important;
99
+ animation: none !important;
100
+ }
101
+
102
+ /* Header */
103
+ .header-container {
104
+ text-align: left;
105
+ margin-bottom: 24px;
106
+ }
107
+
108
+ .main-title {
109
+ font-size: 32px !important;
110
+ font-weight: 600 !important;
111
+ letter-spacing: -0.02em !important;
112
+ line-height: 1.07 !important;
113
+ color: #f5f5f7 !important;
114
+ margin: 0 0 16px 0 !important;
115
+ }
116
+
117
+ /* Input Section */
118
+ .input-section {
119
+ background: #1d1d1f;
120
+ border-radius: 18px;
121
+ padding: 32px;
122
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
123
+ }
124
+
125
+ /* Textbox */
126
+ textarea {
127
+ font-size: 17px !important;
128
+ line-height: 1.47 !important;
129
+ border-radius: 12px !important;
130
+ border: 1px solid #424245 !important;
131
+ padding: 12px 16px !important;
132
+ background: #1d1d1f !important;
133
+ color: #f5f5f7 !important;
134
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif !important;
135
+ min-height: 200px !important;
136
+ max-height: 400px !important;
137
+ height: 200px !important;
138
+ resize: vertical !important;
139
+ overflow-y: auto !important;
140
+ margin-bottom: 16px !important;
141
+ }
142
+
143
+ textarea:focus {
144
+ border-color: #0071e3 !important;
145
+ box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.15) !important;
146
+ outline: none !important;
147
+ }
148
+
149
+ textarea::placeholder {
150
+ color: #86868b !important;
151
+ }
152
+
153
+ /* Button */
154
+ button.primary {
155
+ font-size: 17px !important;
156
+ font-weight: 400 !important;
157
+ padding: 12px 32px !important;
158
+ border-radius: 980px !important;
159
+ background: #0071e3 !important;
160
+ border: none !important;
161
+ color: #ffffff !important;
162
+ min-height: 44px !important;
163
+ letter-spacing: -0.01em !important;
164
+ cursor: pointer !important;
165
+ }
166
+
167
+ button.primary:hover {
168
+ background: #0077ed !important;
169
+ }
170
+
171
+ button.primary:active {
172
+ opacity: 0.9 !important;
173
+ }
174
+
175
+ /* Output Section */
176
+ div.output-section {
177
+ background: #1d1d1f;
178
+ border-radius: 18px;
179
+ padding: 32px;
180
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
181
+ overflow: hidden;
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ min-height: 80vh;
186
+ max-height: 90vh;
187
+ will-change: auto;
188
+ position: relative;
189
+ }
190
+
191
+ .output-section * {
192
+ transform: none !important;
193
+ transition: none !important;
194
+ animation: none !important;
195
+ }
196
+
197
+ .output-section img {
198
+ border-radius: 12px !important;
199
+ max-width: 100% !important;
200
+ max-height: 85vh !important;
201
+ width: auto !important;
202
+ height: auto !important;
203
+ object-fit: contain !important;
204
+ transform: none !important;
205
+ transition: none !important;
206
+ animation: none !important;
207
+ backface-visibility: hidden;
208
+ -webkit-backface-visibility: hidden;
209
+ }
210
+
211
+ /* Make progress/generation area fill more space */
212
+ .output-section > div {
213
+ width: 100% !important;
214
+ min-height: 75vh !important;
215
+ max-height: 85vh !important;
216
+ display: flex !important;
217
+ align-items: center !important;
218
+ justify-content: center !important;
219
+ }
220
+
221
+ .output-section > div > div {
222
+ min-height: 75vh !important;
223
+ max-height: 85vh !important;
224
+ width: 100% !important;
225
+ display: flex !important;
226
+ align-items: center !important;
227
+ justify-content: center !important;
228
+ }
229
+
230
+ .output-section * {
231
+ max-width: 100% !important;
232
+ }
233
+
234
+ /* Progress */
235
+ .progress-bar {
236
+ background: #0071e3 !important;
237
+ border-radius: 4px !important;
238
+ }
239
+
240
+ /* Labels */
241
+ label {
242
+ color: #f5f5f7 !important;
243
+ }
244
+
245
+ /* Dropdown */
246
+ .gr-dropdown {
247
+ background: #1d1d1f !important;
248
+ border: 1px solid #424245 !important;
249
+ border-radius: 12px !important;
250
+ color: #f5f5f7 !important;
251
+ }
252
+
253
+ select, .wrap-inner {
254
+ background: #1d1d1f !important;
255
+ color: #f5f5f7 !important;
256
+ border-color: #424245 !important;
257
+ }
258
+
259
+ /* Fix column width to prevent shrinking */
260
+ .input-section {
261
+ min-width: 550px !important;
262
+ max-width: 550px !important;
263
+ width: 550px !important;
264
+ flex-shrink: 0 !important;
265
+ flex-grow: 0 !important;
266
+ }
267
+
268
+ /* Lock the output section to fill remaining space */
269
+ .output-section {
270
+ flex-grow: 1 !important;
271
+ flex-shrink: 0 !important;
272
+ flex-basis: auto !important;
273
+ }
274
+
275
+ /* Prevent Gradio columns from flexing */
276
+ .gradio-column {
277
+ flex-shrink: 0 !important;
278
+ }
279
+
280
+ /* Stabilize row layout */
281
+ .gradio-row,
282
+ div.gradio-row,
283
+ .gradio-container .gradio-row,
284
+ .gradio-container > .gradio-row,
285
+ .gradio-container div.gradio-row {
286
+ align-items: flex-start !important;
287
+ flex-direction: row !important;
288
+ display: flex !important;
289
+ flex-wrap: nowrap !important;
290
+ width: 100% !important;
291
+ }
292
+
293
+ /* Force columns to stay inline */
294
+ .gradio-row > .gradio-column,
295
+ .gradio-row > div {
296
+ display: inline-flex !important;
297
+ vertical-align: top !important;
298
+ }
299
+
300
+ /* First column - input section */
301
+ .gradio-row > .gradio-column:first-child,
302
+ .gradio-row > div:first-child {
303
+ width: 550px !important;
304
+ min-width: 550px !important;
305
+ max-width: 550px !important;
306
+ flex: 0 0 550px !important;
307
+ }
308
+
309
+ /* Second column - output section */
310
+ .gradio-row > .gradio-column:last-child,
311
+ .gradio-row > div:last-child {
312
+ flex: 1 1 auto !important;
313
+ min-width: 0 !important;
314
+ }
315
+
316
+ /* Hide progress indicator in input section */
317
+ .input-section .progress-container,
318
+ .input-section [class*="progress-bar"],
319
+ .input-section [class*="progress-text"],
320
+ .input-section [class*="progress-level"],
321
+ .input-section .progress,
322
+ .input-section .eta-bar {
323
+ display: none !important;
324
+ visibility: hidden !important;
325
+ height: 0 !important;
326
+ overflow: hidden !important;
327
+ }
328
+
329
+ /* Override responsive behavior */
330
+ @media (max-width: 2000px) {
331
+ .gradio-row,
332
+ div.gradio-row,
333
+ .gradio-container .gradio-row,
334
+ .gradio-container > .gradio-row {
335
+ flex-direction: row !important;
336
+ flex-wrap: nowrap !important;
337
+ display: flex !important;
338
+ }
339
+
340
+ .gradio-row > .gradio-column,
341
+ .gradio-row > div {
342
+ display: inline-flex !important;
343
+ }
344
+
345
+ .gradio-row > .gradio-column:first-child,
346
+ .gradio-row > div:first-child {
347
+ width: 550px !important;
348
+ min-width: 550px !important;
349
+ max-width: 550px !important;
350
+ flex: 0 0 550px !important;
351
+ }
352
+
353
+ .gradio-row > .gradio-column:last-child,
354
+ .gradio-row > div:last-child {
355
+ flex: 1 1 auto !important;
356
+ min-width: 0 !important;
357
+ }
358
+ }
359
+
360
+ /* Responsive text sizing only */
361
+ @media (max-width: 734px) {
362
+ .main-title {
363
+ font-size: 28px !important;
364
+ }
365
+
366
+ .gradio-container {
367
+ padding: 32px 16px !important;
368
+ }
369
 
370
+ .input-section,
371
+ .output-section {
372
+ padding: 24px !important;
373
+ }
374
 
375
+ /* FORCE horizontal layout even on mobile */
376
+ .gradio-row,
377
+ div.gradio-row {
378
+ flex-direction: row !important;
379
+ flex-wrap: nowrap !important;
380
+ }
381
+ }
382
+
383
+ /* Hide Gradio footer */
384
+ footer {
385
+ display: none !important;
386
+ }
387
+
388
+ .footer {
389
+ display: none !important;
390
+ }
391
+ """
392
+
393
+ # JavaScript to force horizontal layout
394
+ js_code = """
395
+ function() {
396
+ function forceHorizontalLayout() {
397
+ // Set container width
398
+ const container = document.querySelector('.gradio-container');
399
+ if (container) {
400
+ container.style.maxWidth = '85vw';
401
+ container.style.width = '85vw';
402
+ }
403
+
404
+ // Target the main row specifically
405
+ const mainRow = document.getElementById('main-row');
406
+ if (mainRow) {
407
+ mainRow.style.flexDirection = 'row';
408
+ mainRow.style.flexWrap = 'nowrap';
409
+ mainRow.style.display = 'flex';
410
+ mainRow.style.width = '100%';
411
+ }
412
+
413
+ // Force ALL rows to stay horizontal
414
+ const rows = document.querySelectorAll('.gradio-row');
415
+ rows.forEach(row => {
416
+ row.style.flexDirection = 'row';
417
+ row.style.flexWrap = 'nowrap';
418
+ row.style.display = 'flex';
419
+ });
420
+
421
+ // Target specific columns
422
+ const inputCol = document.getElementById('input-column');
423
+ if (inputCol) {
424
+ inputCol.style.width = '550px';
425
+ inputCol.style.minWidth = '550px';
426
+ inputCol.style.maxWidth = '550px';
427
+ inputCol.style.flex = '0 0 550px';
428
+ inputCol.style.display = 'inline-flex';
429
+ inputCol.style.flexDirection = 'column';
430
+ }
431
+
432
+ const outputCol = document.getElementById('output-column');
433
+ if (outputCol) {
434
+ outputCol.style.flex = '1 1 auto';
435
+ outputCol.style.minWidth = '0';
436
+ outputCol.style.display = 'inline-flex';
437
+ outputCol.style.flexDirection = 'column';
438
+ }
439
+
440
+ // Fallback: force all column children of rows
441
+ const columns = document.querySelectorAll('.gradio-row > .gradio-column, .gradio-row > div');
442
+ columns.forEach((col, index) => {
443
+ if (index === 0) {
444
+ col.style.width = '550px';
445
+ col.style.minWidth = '550px';
446
+ col.style.maxWidth = '550px';
447
+ col.style.flex = '0 0 550px';
448
+ } else if (index === 1) {
449
+ col.style.flex = '1 1 auto';
450
+ col.style.minWidth = '0';
451
+ }
452
+ col.style.display = 'inline-flex';
453
+ });
454
+ }
455
 
456
+ // Run immediately
457
+ forceHorizontalLayout();
 
 
458
 
459
+ // Run again after delays to override Gradio's dynamic changes
460
+ setTimeout(forceHorizontalLayout, 100);
461
+ setTimeout(forceHorizontalLayout, 500);
462
+ setTimeout(forceHorizontalLayout, 1000);
463
+ setTimeout(forceHorizontalLayout, 2000);
464
 
465
+ // Set up mutation observer to reapply on DOM changes
466
+ const observer = new MutationObserver(forceHorizontalLayout);
467
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
 
468
  }
469
  """
470
 
471
+ # Create the interface
472
+ with gr.Blocks(
473
+ title="LongCat Image",
474
+ fill_height=False,
475
+ ) as demo:
476
+
477
+ # Two-column layout
478
+ with gr.Row(equal_height=False, variant="panel", elem_id="main-row"):
479
+
480
+ # Left column - Input controls (fixed width)
481
+ with gr.Column(scale=0, min_width=550, elem_classes="input-section", elem_id="input-column"):
482
+
483
+ # Title above prompt box
484
+ gr.HTML("""
485
+ <div class="header-container">
486
+ <h1 class="main-title">LongCat Image</h1>
487
+ </div>
488
+ """)
489
+
490
+ prompt = gr.Textbox(
491
+ placeholder="Describe the image you want to create...",
492
+ value="Five shimmering goldfish weave through crevices between stones; four are red-and-white, while one is silver-white. By the pond's edge, a golden shaded British Shorthair cat watches them intently, counting on blind luck. Watercolor style.",
493
+ lines=7,
494
+ max_lines=7,
495
+ label="Prompt",
496
+ show_label=True,
497
+ container=True,
498
+ autoscroll=False,
499
+ )
500
+
501
+ aspect_ratio = gr.Dropdown(
502
+ choices=[
503
+ "1:1 (1024x1024)",
504
+ "4:3 (1024x768)",
505
+ "3:4 (768x1024)",
506
+ "16:9 (1024x576)",
507
+ "9:16 (576x1024)",
508
+ "3:2 (1024x672)",
509
+ "2:3 (672x1024)",
510
+ ],
511
+ value="1:1 (1024x1024)",
512
+ label="Aspect Ratio",
513
+ show_label=True,
514
+ container=True,
515
+ )
516
+
517
+ generate_btn = gr.Button(
518
+ "Generate",
519
+ variant="primary",
520
+ size="lg",
521
+ elem_classes="primary"
522
+ )
523
+
524
+ # Right column - Image output
525
+ with gr.Column(scale=2, elem_classes="output-section", elem_id="output-column"):
526
+ output_image = gr.Image(
527
+ label="Result",
528
+ show_label=False,
529
+ type="pil",
530
+ format="png",
531
+ )
532
+
533
+ # Event handlers
534
+ generate_btn.click(
535
+ fn=generate_image,
536
+ inputs=[prompt, aspect_ratio],
537
+ outputs=output_image,
538
+ show_progress="full"
539
+ )
540
+
541
+ prompt.submit(
542
+ fn=generate_image,
543
+ inputs=[prompt, aspect_ratio],
544
+ outputs=output_image,
545
+ show_progress="full"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  )
547
 
548
+ # Load event to force width with JavaScript
549
+ demo.load(None, None, None, js=js_code)
550
+
551
 
552
  if __name__ == "__main__":
553
+ demo.launch(
554
+ mcp_server=True,
555
+ share=False,
556
+ show_error=True,
557
+ theme=gr.themes.Soft(
558
+ primary_hue=gr.themes.colors.blue,
559
+ secondary_hue=gr.themes.colors.slate,
560
+ neutral_hue=gr.themes.colors.gray,
561
+ spacing_size=gr.themes.sizes.spacing_lg,
562
+ radius_size=gr.themes.sizes.radius_lg,
563
+ text_size=gr.themes.sizes.text_md,
564
+ font=[gr.themes.GoogleFont("Inter"), "SF Pro Display", "-apple-system", "BlinkMacSystemFont", "system-ui", "sans-serif"],
565
+ font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "SF Mono", "ui-monospace", "monospace"],
566
+ ).set(
567
+ body_background_fill='#000000',
568
+ body_background_fill_dark='#000000',
569
+ button_primary_background_fill='#0071e3',
570
+ button_primary_background_fill_hover='#0077ed',
571
+ button_primary_text_color='#ffffff',
572
+ block_background_fill='#1d1d1f',
573
+ block_background_fill_dark='#1d1d1f',
574
+ block_border_width='0px',
575
+ block_shadow='0 2px 12px rgba(0, 0, 0, 0.4)',
576
+ block_shadow_dark='0 2px 12px rgba(0, 0, 0, 0.4)',
577
+ input_background_fill='#1d1d1f',
578
+ input_background_fill_dark='#1d1d1f',
579
+ input_border_width='1px',
580
+ input_border_color='#424245',
581
+ input_border_color_dark='#424245',
582
+ input_shadow='none',
583
+ input_shadow_focus='0 0 0 4px rgba(0, 113, 227, 0.15)',
584
+ ),
585
+ css=apple_css,
586
+ )