shb777 sbera77 notv3nom aarbelle commited on
Commit
7e18529
·
0 Parent(s):

Super-squash branch 'main' using huggingface_hub

Browse files

Co-authored-by: sbera77 <sbera77@users.noreply.huggingface.co>
Co-authored-by: notv3nom <notv3nom@users.noreply.huggingface.co>
Co-authored-by: aarbelle <aarbelle@users.noreply.huggingface.co>

Files changed (5) hide show
  1. .gitattributes +35 -0
  2. README.md +13 -0
  3. app.py +382 -0
  4. requirements.txt +4 -0
  5. style.css +222 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +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
app.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ nltk
2
+ peft
3
+ soundfile
4
+ transformers
style.css ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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; }