Files changed (4) hide show
  1. README.md +6 -6
  2. app.py +99 -359
  3. requirements.txt +6 -4
  4. style.css +0 -222
README.md CHANGED
@@ -1,13 +1,13 @@
1
  ---
2
- title: TinkerSpace
3
- emoji:
4
  colorFrom: indigo
5
  colorTo: green
6
  sdk: gradio
 
7
  app_file: app.py
8
- pinned: true
9
- short_description: Demos for some my finetunes
10
- sdk_version: 6.2.0
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Granite Vision 3.1 2B
3
+ emoji: 👀
4
  colorFrom: indigo
5
  colorTo: green
6
  sdk: gradio
7
+ sdk_version: 5.15.0
8
  app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
 
11
  ---
12
 
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -1,382 +1,122 @@
1
- import re
2
- import nltk
3
- import torch
4
  import spaces
5
-
 
6
  import gradio as gr
 
7
 
8
- from threading import Thread
9
-
10
- from nltk.tag import pos_tag
11
- from nltk.chunk import ne_chunk
12
- from nltk.tokenize import word_tokenize
13
-
14
- from peft import PeftModel
15
- from transformers import CsmForConditionalGeneration
16
- from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer, AutoProcessor
17
-
18
- try:
19
- nltk.data.find('tokenizers/punkt')
20
- nltk.data.find('taggers/averaged_perceptron_tagger')
21
- nltk.data.find('chunkers/maxent_ne_chunker')
22
- nltk.data.find('corpora/words')
23
- except LookupError:
24
- nltk.download('punkt', quiet=True)
25
- nltk.download('averaged_perceptron_tagger', quiet=True)
26
- nltk.download('maxent_ne_chunker', quiet=True)
27
- nltk.download('words', quiet=True)
28
-
29
-
30
- SYSTEM_PROMPT = """You are an expert creative director specializing in visual descriptions for image generation.
31
-
32
- Your task: Transform the user's concept into a rich, detailed image description while PRESERVING their core idea.
33
-
34
- IMPORTANT RULES:
35
- 1. Keep ALL key elements (intents, entities) from the original concept
36
- 2. Enhance with artistic details, NOT change the fundamental idea
37
- 3. Maintain the user's intended subject, action, and setting
38
-
39
- You should elaborate on:
40
- • Visual composition and perspective (bird's eye, close-up, wide angle, etc.)
41
- • Artistic style (photorealistic, impressionist, specific artist like Van Gogh, etc.)
42
- • Color palette and color temperature
43
- • Lighting (golden hour, dramatic shadows, soft diffused, etc.)
44
- • Atmosphere and mood
45
- • Textures and materials (rough, smooth, metallic, organic, etc.)
46
- • Technical details (medium, brushwork, rendering style)
47
- • Environmental context (time of day, weather, season, era)
48
- • Level of detail and focus points
49
-
50
- Output format: A single, flowing paragraph that reads naturally as an image prompt."""
51
-
52
- prompt_model = AutoModelForCausalLM.from_pretrained("shb777/PromptTuner-v0.1")
53
- tokenizer = AutoTokenizer.from_pretrained("shb777/PromptTuner-v0.1")
54
- prompt_model.eval()
55
-
56
- CSM_BASE_MODEL_ID = "sesame/csm-1b"
57
- CSM_ADAPTER_ID = "shb777/csm-maya-exp2"
58
- SPEAKER_ID = 4 # Was trained on this speaker ID
59
- device = "cuda" if torch.cuda.is_available() else "cpu"
60
-
61
- csm_processor = AutoProcessor.from_pretrained(CSM_BASE_MODEL_ID)
62
- csm_model = CsmForConditionalGeneration.from_pretrained(CSM_BASE_MODEL_ID, device_map=device)
63
- csm_model = PeftModel.from_pretrained(csm_model, CSM_ADAPTER_ID)
64
- csm_model.eval()
65
-
66
-
67
- def extract_key_phrases(text: str) -> list: # We will highlight key phrases in the enhanced prompt
68
- phrases = []
69
- try:
70
- tokens = word_tokenize(text)
71
- tagged = pos_tag(tokens)
72
- chunks = ne_chunk(tagged)
73
-
74
- current_phrase = []
75
- for chunk in chunks:
76
- if hasattr(chunk, 'label'):
77
- phrase = ' '.join([token for token, _ in chunk.leaves()])
78
- phrases.append(phrase.lower())
79
- elif chunk[1].startswith('NN'):
80
- current_phrase.append(chunk[0])
81
- elif chunk[1].startswith('JJ') and current_phrase:
82
- current_phrase.append(chunk[0])
83
- else:
84
- if current_phrase:
85
- phrases.append(' '.join(current_phrase).lower())
86
- current_phrase = []
87
-
88
- if current_phrase:
89
- phrases.append(' '.join(current_phrase).lower())
90
-
91
- for word, tag in tagged:
92
- if tag.startswith('JJ') or tag in ('RB', 'RBR', 'RBS'):
93
- phrases.append(word.lower())
94
-
95
- except Exception:
96
- words = re.findall(r'\b[a-zA-Z]{3,}\b', text.lower())
97
- phrases = list(set(words))
98
-
99
- multi_word = re.findall(r'\b[a-zA-Z]{3,}(?:\s+[a-zA-Z]{3,}){1,3}\b', text)
100
- phrases.extend([mw.lower() for mw in multi_word])
101
-
102
- phrases = list(set(phrases))
103
- phrases.sort(key=len, reverse=True)
104
-
105
- return phrases[:20]
106
-
107
-
108
- def highlight_matches(original_input: str, enhanced_output: str) -> str:
109
- if not original_input.strip():
110
- return f'<p class="output-text">{enhanced_output}</p>'
111
-
112
- key_phrases = extract_key_phrases(original_input)
113
- if not key_phrases:
114
- return f'<p class="output-text">{enhanced_output}</p>'
115
-
116
- key_phrases.sort(key=len, reverse=True)
117
-
118
- output = enhanced_output
119
- highlighted_spans = []
120
-
121
- for phrase in key_phrases:
122
- pattern = re.compile(r'\b' + re.escape(phrase) + r'\b', re.IGNORECASE)
123
-
124
- def replace_with_highlight(match):
125
- matched_text = match.group(0)
126
- start = match.start()
127
- for h_start, h_end in highlighted_spans:
128
- if start >= h_start and start <= h_end:
129
- return matched_text
130
- highlighted_spans.append((start, match.end()))
131
- return f'<mark class="highlight-keyword">{matched_text}</mark>'
132
-
133
- output = pattern.sub(replace_with_highlight, output)
134
-
135
- return f'<p class="output-text">{output}</p>'
136
-
137
-
138
- def enhance_prompt(user_prompt: str):
139
- if not user_prompt or not user_prompt.strip():
140
- yield (
141
- '<span class="placeholder-text">Please enter a prompt to enhance.</span>',
142
- "",
143
- gr.update(interactive=True),
144
- gr.update(interactive=True)
145
- )
146
- return
147
-
148
- messages = [
149
- {"role": "system", "content": SYSTEM_PROMPT},
150
- {"role": "user", "content": user_prompt}
151
- ]
152
-
153
- prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
154
- inputs = tokenizer(prompt, return_tensors="pt")
155
-
156
- streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True, skip_prompt=True)
157
- generation_kwargs = {
158
- 'max_new_tokens': 512,
159
- 'streamer': streamer,
160
- 'do_sample': True,
161
- 'temperature': 1,
162
- 'top_p': 0.95,
163
- 'top_k': 64
164
- }
165
-
166
- placeholder = '<span class="placeholder-text">Your enhanced prompt will appear here</span>'
167
- yield placeholder, "", gr.update(interactive=False), gr.update(interactive=False)
168
-
169
- thread = Thread(target=prompt_model.generate, kwargs={**inputs, **generation_kwargs})
170
- thread.start()
171
-
172
- output = ""
173
- for text in streamer:
174
- output += text
175
- highlighted = highlight_matches(user_prompt, output)
176
- yield highlighted, output, gr.update(), gr.update()
177
-
178
- final_highlighted = highlight_matches(user_prompt, output)
179
- yield final_highlighted, output, gr.update(interactive=True), gr.update(interactive=True)
180
 
 
 
 
 
 
 
 
 
181
 
182
  @spaces.GPU
183
- def generate_tts_gpu(text):
184
- conversation = [
185
- {"role": str(SPEAKER_ID), "content": [{"type": "text", "text": text}]},
186
- ]
187
-
188
- enc = csm_processor.apply_chat_template(
 
 
 
 
 
 
 
 
 
 
 
 
189
  conversation,
 
190
  tokenize=True,
191
  return_dict=True,
192
- return_tensors="pt",
193
- ).to(device)
194
-
195
- gen_kwargs = {
196
- "max_new_tokens": 375,
197
- # "do_sample": True,
198
- # "temperature": 0.7,
199
- # "depth_decoder_do_sample": True,
200
- # "depth_decoder_temperature": 0.7,
201
- # "depth_decoder_top_k": 20,
202
- # "depth_decoder_top_p": 0.95,
203
- }
204
-
205
- audio = csm_model.generate(
206
- **enc,
207
- **gen_kwargs,
208
- output_audio=True
209
- )
210
- audio_array = audio[0].to(torch.float32).cpu().numpy()
211
- return (24000, audio_array)
212
-
213
-
214
- def text_to_speech(text: str):
215
- if not text or not text.strip():
216
- raise gr.Error("Please enter text to convert to speech.")
217
-
218
- if len(text) > 200:
219
- raise gr.Error("Text too long. Please limit to 200 characters or split into sentences.")
220
-
221
- # Preprocess text - remove characters that CSM struggles with
222
- text = text.replace('(', '').replace(')', '')
223
- text = text.replace('"', '').replace('"', '').replace('"', '')
224
- text = text.replace(';', ',')
225
- text = text.replace('!', ' ')
226
- text = text.replace('[', '').replace(']', '')
227
- text = text.replace('/', ' ')
228
-
229
- try:
230
- audio_result = generate_tts_gpu(text)
231
- return audio_result
232
- except Exception as e:
233
- raise gr.Error(f"Failed to generate speech: {str(e)}")
234
-
235
- with open("style.css", "r") as f: # Load CSS for styling
236
- custom_css = f.read()
237
 
238
- with gr.Blocks(css=custom_css, title="TinkerSpace") as demo:
239
 
240
- with gr.Tabs():
241
- with gr.Tab("CSM Maya TTS"):
242
- with gr.Row(elem_classes=["main-grid"]):
243
- with gr.Column(elem_classes=["card"]):
244
- gr.HTML('<label class="form-label">Text Input</label>')
245
-
246
- tts_input = gr.Textbox(
247
- placeholder="Enter text to convert to speech...",
248
- lines=5,
249
- show_label=False,
250
- container=False,
251
- elem_classes=["input-textarea"]
252
- )
253
-
254
- with gr.Row(elem_classes=["flex gap-2 mt-6"]):
255
- generate_tts_btn = gr.Button(
256
- "Generate Speech",
257
- variant="primary",
258
- scale=2,
259
- elem_classes=["btn", "btn-primary"]
260
- )
261
- clear_tts_btn = gr.Button(
262
- "Clear",
263
- scale=1,
264
- elem_classes=["btn", "btn-secondary"]
265
- )
266
-
267
- with gr.Column(elem_classes=["card"]):
268
- gr.HTML('<label class="form-label">Generated Audio</label>')
269
-
270
- tts_output = gr.Audio(
271
- label=None,
272
- show_label=False,
273
- autoplay=False,
274
- interactive=False,
275
- elem_classes=["output-container"]
276
- )
277
-
278
- with gr.Column(elem_classes=["examples-section"]):
279
- gr.Examples(
280
- examples=[
281
- ['You went to the party, even though I explicitly told you not to?'],
282
- ["With a gentle touch and a loving smile, she reassured, 'Dont worry, my love. We'll get through this together, just like we always have. I love you.'"],
283
- ["After years of work, Heisenberg finally published a ground-breaking cutting-edge research paper on quantum physics."],
284
- ['"Uh, are you sure about this?" Tim asked nervously, looking at the steep slope before them. "Whoa, it\'s higher than I thought," he continued, his voice filled with trepidation.'],
285
- ['"Shh, Lucy, shh, we mustn\'t wake your baby brother," Tom whispered, as they tiptoed past the nursery.']
286
- ],
287
- inputs=tts_input,
288
- label="Examples"
289
- )
290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  with gr.Row():
292
- gr.Markdown(
293
- "Powered by [CSM Maya](https://huggingface.co/shb777/csm-maya-exp2)"
294
- )
295
-
296
- with gr.Tab("Prompt Enhancer"):
297
- with gr.Row(elem_classes=["main-grid"]):
298
- with gr.Column(elem_classes=["card"]):
299
- gr.HTML('<label class="form-label">Input Prompt</label>')
300
-
301
- input_text = gr.Textbox(
302
- placeholder="Describe your image concept... e.g., fox, red tail, blue moon, clouds",
303
- lines=5,
304
- show_label=False,
305
- autofocus=True,
306
- container=False,
307
- elem_classes=["input-textarea"]
308
- )
309
 
310
- with gr.Row(elem_classes=["flex gap-2 mt-6"]):
311
- enhance_btn = gr.Button(
312
- "Enhance Prompt",
313
- variant="primary",
314
- scale=2,
315
- elem_classes=["btn", "btn-primary"]
316
- )
317
- clear_btn = gr.Button(
318
- "Clear",
319
- scale=1,
320
- elem_classes=["btn", "btn-secondary"]
321
- )
322
 
323
- with gr.Column(elem_classes=["card"]):
324
- gr.HTML('<label class="form-label">Enhanced Prompt</label>')
325
-
326
- output_html = gr.HTML(
327
- value='<span class="placeholder-text">Your enhanced prompt will appear here</span>',
328
- elem_classes=["output-container"]
329
- )
330
-
331
- raw_output = gr.Textbox(visible=False)
332
-
333
- with gr.Column(elem_classes=["examples-section"]):
334
- gr.Examples(
335
- examples=[
336
- ["fox, red tail, blue moon, clouds"],
337
- ["room with french window, cozy morning vibes, minimal"],
338
- ["anime style, sunset, japan"]
339
- ],
340
- inputs=input_text,
341
- label="Examples"
342
- )
343
-
344
- with gr.Row():
345
- gr.Markdown(
346
- "Powered by [PromptTuner](https://huggingface.co/shb777/PromptTuner-v0.1), "
347
- "a finetuned gemma3-270M model specifically designed to enhance text prompts "
348
- "for text-to-image generation."
349
- )
350
-
351
- enhance_btn.click(
352
- fn=enhance_prompt,
353
- inputs=[input_text],
354
- outputs=[output_html, raw_output, enhance_btn, clear_btn]
355
  )
356
 
357
- clear_btn.click(
358
- fn=lambda: (
359
- "",
360
- '<span class="placeholder-text">Your enhanced prompt will appear here</span>',
361
- "",
362
- gr.update(interactive=True),
363
- gr.update(interactive=True)
364
- ),
365
  inputs=None,
366
- outputs=[input_text, output_html, raw_output, enhance_btn, clear_btn]
367
- )
368
-
369
- generate_tts_btn.click(
370
- fn=text_to_speech,
371
- inputs=[tts_input],
372
- outputs=[tts_output]
373
  )
374
 
375
- clear_tts_btn.click(
376
- fn=lambda: ("", None),
377
- inputs=None,
378
- outputs=[tts_input, tts_output]
 
379
  )
380
 
381
  if __name__ == "__main__":
382
- demo.queue(max_size=20).launch(mcp_server=True)
 
 
 
 
1
  import spaces
2
+ import random
3
+ import torch
4
  import gradio as gr
5
+ from transformers import LlavaNextProcessor, LlavaNextForConditionalGeneration
6
 
7
+ model_path = "ibm-granite/granite-vision-3.1-2b-preview"
8
+ processor = LlavaNextProcessor.from_pretrained(model_path, use_fast=True)
9
+ model = LlavaNextForConditionalGeneration.from_pretrained(model_path, torch_dtype="auto", device_map="auto")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ def get_text_from_content(content):
12
+ texts = []
13
+ for item in content:
14
+ if item["type"] == "text":
15
+ texts.append(item["text"])
16
+ elif item["type"] == "image":
17
+ texts.append("<image>")
18
+ return " ".join(texts)
19
 
20
  @spaces.GPU
21
+ def chat_inference(image, text, temperature, top_p, top_k, max_tokens, conversation):
22
+ if conversation is None:
23
+ conversation = []
24
+
25
+ user_content = []
26
+ if image is not None:
27
+ user_content.append({"type": "image", "image": image})
28
+ if text and text.strip():
29
+ user_content.append({"type": "text", "text": text.strip()})
30
+ if not user_content:
31
+ return conversation_display(conversation), conversation
32
+
33
+ conversation.append({
34
+ "role": "user",
35
+ "content": user_content
36
+ })
37
+
38
+ inputs = processor.apply_chat_template(
39
  conversation,
40
+ add_generation_prompt=True,
41
  tokenize=True,
42
  return_dict=True,
43
+ return_tensors="pt"
44
+ ).to("cuda")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ torch.manual_seed(random.randint(0, 10000))
47
 
48
+ generation_kwargs = {
49
+ "max_new_tokens": max_tokens,
50
+ "temperature": temperature,
51
+ "top_p": top_p,
52
+ "top_k": top_k,
53
+ "do_sample": True,
54
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ output = model.generate(**inputs, **generation_kwargs)
57
+ assistant_response = processor.decode(output[0], skip_special_tokens=True)
58
+
59
+ conversation.append({
60
+ "role": "assistant",
61
+ "content": [{"type": "text", "text": assistant_response.strip()}]
62
+ })
63
+
64
+ return conversation_display(conversation), conversation
65
+
66
+ def conversation_display(conversation):
67
+ chat_history = []
68
+ for msg in conversation:
69
+ if msg["role"] == "user":
70
+ user_text = get_text_from_content(msg["content"])
71
+ elif msg["role"] == "assistant":
72
+ assistant_text = msg["content"][0]["text"].split("<|assistant|>")[-1].strip()
73
+ chat_history.append({"role": "user", "content": user_text})
74
+ chat_history.append({"role": "assistant", "content": assistant_text})
75
+ return chat_history
76
+
77
+ def clear_chat():
78
+ return [], [], "", None
79
+
80
+ with gr.Blocks(title="Granite Vision 3.1 2B", css="h1 { overflow: hidden; }") as demo:
81
+ gr.Markdown("# Granite Vision 3.1 2B")
82
+
83
+ with gr.Row():
84
+ with gr.Column(scale=2):
85
+ image_input = gr.Image(type="pil", label="Upload Image (optional)")
86
+ with gr.Column():
87
+ temperature_input = gr.Slider(minimum=0.0, maximum=2.0, value=0.2, step=0.01, label="Temperature")
88
+ top_p_input = gr.Slider(minimum=0.0, maximum=1.0, value=0.95, step=0.01, label="Top p")
89
+ top_k_input = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="Top k")
90
+ max_tokens_input = gr.Slider(minimum=10, maximum=300, value=128, step=1, label="Max Tokens")
91
+
92
+ with gr.Column(scale=3):
93
+ chatbot = gr.Chatbot(label="Chat History", elem_id="chatbot", type='messages')
94
+ text_input = gr.Textbox(lines=2, placeholder="Enter your message here", label="Message")
95
  with gr.Row():
96
+ send_button = gr.Button("Chat")
97
+ clear_button = gr.Button("Clear Chat")
98
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ state = gr.State([])
 
 
 
 
 
 
 
 
 
 
 
101
 
102
+ send_button.click(
103
+ chat_inference,
104
+ inputs=[image_input, text_input, temperature_input, top_p_input, top_k_input, max_tokens_input, state],
105
+ outputs=[chatbot, state]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  )
107
 
108
+ clear_button.click(
109
+ clear_chat,
 
 
 
 
 
 
110
  inputs=None,
111
+ outputs=[chatbot, state, text_input, image_input]
 
 
 
 
 
 
112
  )
113
 
114
+ gr.Examples(
115
+ examples=[
116
+ ["https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png", "What is this?"]
117
+ ],
118
+ inputs=[image_input, text_input]
119
  )
120
 
121
  if __name__ == "__main__":
122
+ demo.launch()
requirements.txt CHANGED
@@ -1,4 +1,6 @@
1
- nltk
2
- peft
3
- soundfile
4
- transformers
 
 
 
1
+ torch
2
+ torchvision
3
+ git+https://github.com/huggingface/transformers.git
4
+ gradio
5
+ accelerate
6
+ bitsandbytes
style.css DELETED
@@ -1,222 +0,0 @@
1
- /* ========== CSS VARIABLES ========== */
2
- :root {
3
- --background: 240 10% 3.9%;
4
- --foreground: 0 0% 98%;
5
- --card: 240 10% 4.5%;
6
- --card-border: 240 3.7% 18%;
7
- --primary: 0 0% 98%;
8
- --primary-foreground: 240 5.9% 10%;
9
- --secondary: 240 3.7% 15.9%;
10
- --secondary-foreground: 0 0% 98%;
11
- --muted: 240 3.7% 15.9%;
12
- --muted-foreground: 240 5% 64.9%;
13
- --accent: 240 3.7% 15.9%;
14
- --accent-foreground: 0 0% 98%;
15
- --border: 240 3.7% 18%;
16
- --input: 240 3.7% 18%;
17
- --ring: 240 5.9% 85%;
18
- --radius: 0.625rem;
19
- }
20
-
21
- /* ========== GLOBAL STYLES ========== */
22
- .gradio-container {
23
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
24
- background: hsl(var(--background)) !important;
25
- color: hsl(var(--foreground));
26
- }
27
-
28
- .gradio-container mark {
29
- background: hsl(var(--accent) / 0.6);
30
- color: hsl(var(--accent-foreground));
31
- padding: 0.15em 0.35em;
32
- border-radius: calc(var(--radius) - 2px);
33
- font-weight: 500;
34
- border: 1px solid hsl(var(--border) / 0.5);
35
- }
36
-
37
- footer { display: none !important; }
38
-
39
- /* ========== MARKDOWN ========== */
40
- .gradio-markdown {
41
- color: hsl(var(--foreground)) !important;
42
- font-size: 0.9375rem !important;
43
- line-height: 1.6 !important;
44
- }
45
-
46
- .gradio-markdown:first-child {
47
- margin-bottom: 2rem;
48
- padding-bottom: 1.5rem;
49
- border-bottom: 1px solid hsl(var(--border));
50
- }
51
-
52
- .gradio-markdown:last-child {
53
- padding-top: 1.5rem;
54
- border-top: 1px solid hsl(var(--border));
55
- color: hsl(var(--muted-foreground)) !important;
56
- }
57
-
58
- .gradio-markdown a {
59
- color: hsl(var(--foreground)) !important;
60
- text-decoration: none;
61
- border-bottom: 1px solid hsl(var(--border));
62
- transition: border-color 0.2s ease;
63
- }
64
-
65
- .gradio-markdown a:hover {
66
- border-color: hsl(var(--ring));
67
- }
68
-
69
- /* ========== LAYOUT ========== */
70
- .main-grid {
71
- display: grid;
72
- grid-template-columns: 1fr 1fr;
73
- gap: 2rem;
74
- }
75
-
76
- @media (max-width: 768px) {
77
- .main-grid { grid-template-columns: 1fr; }
78
- }
79
-
80
- /* ========== CARDS ========== */
81
- .card {
82
- background: hsl(var(--card));
83
- border: 1px solid hsl(var(--card-border));
84
- border-radius: var(--radius);
85
- padding: 1.5rem;
86
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
87
- }
88
-
89
- /* ========== FORM ELEMENTS ========== */
90
- .form-label {
91
- font-size: 0.875rem;
92
- font-weight: 500;
93
- margin-bottom: 0.5rem;
94
- display: block;
95
- color: hsl(var(--foreground));
96
- }
97
-
98
- .input-textarea {
99
- width: 100%;
100
- min-height: 140px;
101
- padding: 0.875rem;
102
- font-size: 0.9375rem;
103
- line-height: 1.6;
104
- background: hsl(var(--background));
105
- border: 1px solid hsl(var(--input));
106
- border-radius: var(--radius);
107
- color: hsl(var(--foreground));
108
- transition: all 0.2s ease;
109
- resize: vertical;
110
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
111
- }
112
-
113
- .input-textarea::placeholder {
114
- color: hsl(var(--muted-foreground) / 0.7);
115
- }
116
-
117
- .input-textarea:focus {
118
- outline: none;
119
- border-color: hsl(var(--ring));
120
- box-shadow: 0 0 0 3px hsl(var(--ring) / 0.1), 0 1px 2px rgba(0, 0, 0, 0.2);
121
- background: hsl(var(--background) / 0.8);
122
- }
123
-
124
- /* ========== BUTTONS ========== */
125
- .btn {
126
- display: inline-flex;
127
- align-items: center;
128
- justify-content: center;
129
- gap: 0.5rem;
130
- font-size: 0.9375rem;
131
- font-weight: 500;
132
- padding: 0.625rem 1.25rem;
133
- border-radius: var(--radius);
134
- cursor: pointer;
135
- transition: all 0.2s ease;
136
- border: none;
137
- }
138
-
139
- .btn:focus-visible {
140
- outline: none;
141
- box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--ring));
142
- }
143
-
144
- .btn-primary {
145
- background: hsl(var(--primary));
146
- color: hsl(var(--primary-foreground));
147
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.05) inset;
148
- }
149
-
150
- .btn-primary:hover {
151
- opacity: 0.95;
152
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
153
- }
154
-
155
- .btn-primary:active {
156
- transform: translateY(1px);
157
- }
158
-
159
- .btn-primary:disabled {
160
- opacity: 0.5;
161
- cursor: not-allowed;
162
- }
163
-
164
- .btn-secondary {
165
- background: hsl(var(--secondary));
166
- color: hsl(var(--secondary-foreground));
167
- border: 1px solid hsl(var(--border));
168
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
169
- }
170
-
171
- .btn-secondary:hover {
172
- background: hsl(var(--secondary) / 0.8);
173
- border-color: hsl(var(--muted-foreground) / 0.5);
174
- }
175
-
176
- .btn-secondary:active {
177
- transform: translateY(1px);
178
- }
179
-
180
- /* ========== OUTPUT CONTAINER ========== */
181
- .output-container {
182
- min-height: 140px;
183
- padding: 0.875rem;
184
- border: 1px solid hsl(var(--input));
185
- border-radius: var(--radius);
186
- background: hsl(var(--background));
187
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
188
- }
189
-
190
- .output-text {
191
- color: hsl(var(--foreground));
192
- font-size: 0.9375rem;
193
- line-height: 1.75;
194
- margin: 0;
195
- }
196
-
197
- .placeholder-text {
198
- color: hsl(var(--muted-foreground));
199
- }
200
-
201
- .highlight-keyword {
202
- background: hsl(var(--accent) / 0.6);
203
- color: hsl(var(--accent-foreground));
204
- padding: 0.15em 0.35em;
205
- border-radius: calc(var(--radius) - 2px);
206
- font-weight: 500;
207
- border: 1px solid hsl(var(--border) / 0.5);
208
- }
209
-
210
- /* ========== EXAMPLES ========== */
211
- .examples-section {
212
- padding: 1.5rem;
213
- background: hsl(var(--card));
214
- border: 1px solid hsl(var(--card-border));
215
- border-radius: var(--radius);
216
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
217
- }
218
-
219
- /* ========== SPACING UTILITIES ========== */
220
- .mt-6 { margin-top: 1.5rem; }
221
- .flex { display: flex; }
222
- .gap-2 { gap: 0.5rem; }