crystaljhollis commited on
Commit
99efcd0
·
verified ·
1 Parent(s): 58e918c

Proof of Concept

Browse files
Files changed (3) hide show
  1. main.py +445 -0
  2. readme.md +20 -0
  3. requirements.txt +13 -0
main.py ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Main.py by Crystal J. Hollis | crystaljhollis@gmail.com
2
+ # GitHub: https://github.com/crystaljhollis
3
+ # 08/2025
4
+ # Developed using Python 3.13.7
5
+ #
6
+
7
+ # =======================================================================================================================================
8
+ # LIBRARIES
9
+ # =======================================================================================================================================
10
+
11
+ # Import Python Standard Libraries
12
+ import re #regular expressions aka regex
13
+ from pathlib import Path #file system paths
14
+ from typing import List, Tuple, Set #gradual typing support
15
+
16
+ # Import gradio
17
+ import gradio as gr # Documentation: https://www.gradio.app/
18
+
19
+ # =======================================================================================================================================
20
+ # FUNCTIONS
21
+ # =======================================================================================================================================
22
+
23
+ # HELPER FUNCTIONS
24
+ _SLUG_OK = re.compile(r"[^a-z0-9_]+")
25
+
26
+ def slugify(text: str) -> str:
27
+ """lowercase, spaces->underscore, drop junk"""
28
+ s = text.strip().lower()
29
+ s = s.replace("&", " and ")
30
+ s = re.sub(r"[^\w\s-]", "", s) # keep letters/numbers/underscore/hyphen/space
31
+ s = re.sub(r"\s+", "_", s) # collapse spaces to _
32
+ s = _SLUG_OK.sub("_", s) # anything else -> _
33
+ s = re.sub(r"_+", "_", s).strip("_") # collapse repeats
34
+ return s
35
+
36
+ def suggest_slugs(locked_people: List[str], locked_locs: List[str], rough_terms: List[str]) -> List[str]:
37
+ """
38
+ Very simple slug ideas using location + common event words.
39
+ Swap this for an LLM later if you want.
40
+ """
41
+ loc = locked_locs[0] if locked_locs else ""
42
+ base = slugify(loc) if loc else "gallery_item"
43
+
44
+ # pick a few common event-ish tags if present
45
+ vocab = ["opening ceremony", "ribbon cutting", "audience", "ceremony", "speeches", "panel", "performance"]
46
+ hits = [v for v in vocab if any(v in t.lower() for t in rough_terms)]
47
+
48
+ slugs = [f"{base}_{slugify(h)}" for h in hits[:3]] or [base]
49
+ # add a couple safe fallbacks
50
+ return list(dict.fromkeys(slugs + [f"{base}_event", f"{base}_scene"]))
51
+
52
+ def suggest_headline_title(locked_locs: List[str], rough_terms: List[str]) -> Tuple[str, str]:
53
+ """
54
+ Headline = longer, Title = shorter. Heuristics only.
55
+ """
56
+ loc = locked_locs[0] if locked_locs else "Campus"
57
+ if any("ribbon" in t.lower() and "cut" in t.lower() for t in rough_terms):
58
+ headline = f"{loc} Opening Ceremony and Ribbon Cutting at SMU"
59
+ title = f"{loc} Opening Ceremony"
60
+ else:
61
+ headline = f"Event at {loc}, Southern Methodist University"
62
+ title = f"{loc} Event"
63
+ return headline, title
64
+
65
+ def suggest_description(locked_locs: List[str], rough_terms: List[str]) -> str:
66
+ """
67
+ Template now; replace with Gemma call later.
68
+ """
69
+ loc = locked_locs[0] if locked_locs else "campus venue"
70
+ phrases = [t for t in rough_terms if len(t.split()) <= 4][:6]
71
+ core = ", ".join(phrases) if phrases else "program footage, speeches, audience"
72
+ return (
73
+ f"Program footage from the {loc} event at Southern Methodist University, featuring {core} "
74
+ f"in Dallas, Texas."
75
+ )
76
+
77
+ def people_variants_stub(people_list: List[str]) -> str:
78
+ """
79
+ Placeholder until Gemma is wired. Produces generic roles.
80
+ """
81
+ if not people_list:
82
+ return ""
83
+ base = []
84
+ # naive role guessers
85
+ hints = ["president", "dean", "trustee", "donor", "student", "faculty"]
86
+ base.extend({h for h in hints})
87
+ return ", ".join(sorted(set(base)))
88
+
89
+
90
+ # SPLIT TERMS FUNCTION == accepts text and returns a cleaned list
91
+ def _split_terms(text: str) -> List[str]:
92
+ """
93
+ Accepts comma/newline/semicolon separated text and returns a cleaned list.
94
+ - trims spaces
95
+ - collapses internal whitespace
96
+ - removes surrounding quotes
97
+ - keeps hyphens and apostrophes (useful for names/places)
98
+ """
99
+ if not text:
100
+ return [] # Empty text returns empty list
101
+ parts = re.split(r"[,\n;]+", text) # regex split on commas, semicolons, or newlines
102
+ cleaned = [] # Prepare list to hold cleaned-up terms
103
+ for p in parts: # Iterates through each text piece
104
+ t = p.strip().strip('"').strip("'") # removes trailing spaces, double/single quotes
105
+ # collapse extra internal spaces
106
+ t = re.sub(r"\s+", " ", t) # regex sub whitespace w/single space (\s+)
107
+ if t:
108
+ cleaned.append(t) # If t is not empty, append to the cleaned list
109
+ return cleaned # Returns the cleaned list
110
+
111
+
112
+
113
+ # LOAD MASTER KEYWORDS FUNCTION == user uploads txt file
114
+ def _load_master_keywords(file_obj) -> Set[str]: # Accepts .txt, returns lowercase keywords
115
+ """
116
+ Load hierarchical SMU keyword file.
117
+ - Keeps each line as its own entry
118
+ - Ignores bracketed category headers like [Academic Life]
119
+ - Strips tabs/spaces
120
+
121
+ """
122
+ if not file_obj:
123
+ return set()
124
+
125
+ if isinstance(file_obj, bytes):
126
+ content = file_obj.decode("utf-8", errors="ignore") # Decode file_obj into string with UTF-8 encoding, ignore weird characters
127
+ else:
128
+ content = str(file_obj) #force to string
129
+ keywords = set()
130
+ for line in content.splitlines(): # Iterates line by line
131
+ line = line.strip()
132
+ if not line:
133
+ continue # Ignores blank lines
134
+ # skip category headers in [brackets]
135
+ if line.startswith("[") and line.endswith("]"):
136
+ continue # Ignores [Category] headers
137
+ keywords.add(line.lower()) # Preserve keywords, matching lowercase
138
+ return keywords # Returns the cleaned set of keywords
139
+
140
+
141
+ # COMMA+SPACE FUNCTION == simply adds comma and space for each keyword
142
+ def _to_csv_line(items: List[str]) -> str: # Adds comma and space to cleaned list
143
+ """
144
+ Make a comma+space separated line
145
+ for copy-paste.
146
+ """
147
+ return ", ".join(items) # Concatenates all elements separated by ", "
148
+
149
+
150
+ # KEYWORD CHECKER FUNCTION
151
+ def keyword_checker( # Takes arguments:
152
+ job_number: str, # Job Number i.e. 26-999
153
+ neg_number: str, # Neg Number
154
+ rough_keywords: str, # Rough draft keywords
155
+ people_locked: str, # Names of Persons Shown
156
+ locations_locked: str, # Names of Locations
157
+ names_locked: str, # Other locked terms
158
+ master_txt # Latest keyword txt file
159
+ ):
160
+ # [1] Parse inputs; Converts raw text into a list, calling _split_terms
161
+ rough_list = _split_terms(rough_keywords)
162
+ people_list = _split_terms(people_locked)
163
+ locations_list = _split_terms(locations_locked)
164
+ names_list = _split_terms(names_locked)
165
+
166
+ # [2] Build locked sets (lowercase for matching)
167
+ locked_map = { # Dictionary
168
+ "PEOPLE": set(p.lower() for p in people_list), # lowercase so matching is case-insensitive
169
+ "LOCATIONS": set(l.lower() for l in locations_list),
170
+ "LOCKED": set(a.lower() for a in names_list),
171
+ }
172
+ locked_all = set().union(*locked_map.values()) # Combines everything in the dictionary, keeping only unique elements
173
+
174
+ # [3] Load master keyword list
175
+ master_set = _load_master_keywords(master_txt) if master_txt else set() # Load keyword txt if provided, else empty set
176
+
177
+ # [4] Categorize
178
+ locked_hits = {"PEOPLE": [], "LOCATIONS": [], "LOCKED": []} # locked terms separated by category
179
+ in_master = [] # from keyword txt file
180
+ new_terms = [] # not in keyword txt file
181
+
182
+ for kw in rough_list:
183
+ k = kw.lower() # loop through each keywords, make it lowercase
184
+ # Check locked first (so they can be handled specially later)
185
+ hit_locked = False # checked if keyword is in a locked category
186
+ for cat, s in locked_map.items():
187
+ if k in s:
188
+ locked_hits[cat].append(kw) # If yes, add to locked_hits
189
+ hit_locked = True
190
+ break
191
+ if hit_locked: # locked terms don't need to be checked against keyword txt file
192
+ continue
193
+
194
+ # Then check against master
195
+ if master_set: # check if in the keyword txt file
196
+ if k in master_set:
197
+ in_master.append(kw) # If yes, in_master
198
+ else:
199
+ new_terms.append(kw) # If no, new_terms
200
+ else:
201
+ # No master list provided
202
+ in_master.append(kw) # If no keyword txt file, treat all non-locked terms as in_master
203
+
204
+ # [5] Build human-readable outputs
205
+ locked_block = {
206
+ "PEOPLE": _to_csv_line(locked_hits["PEOPLE"]),
207
+ "LOCATIONS": _to_csv_line(locked_hits["LOCATIONS"]),
208
+ "LOCKED": _to_csv_line(locked_hits["LOCKED"]),
209
+ }
210
+ in_list_block = _to_csv_line(in_master)
211
+ new_block = _to_csv_line(new_terms)
212
+ combined_csv = _to_csv_line(locked_hits["PEOPLE"] + locked_hits["LOCATIONS"] + locked_hits["LOCKED"] + in_master + new_terms) # Combined summary output
213
+
214
+ # [6] LLM customizable fields
215
+ slugs = suggest_slugs(locked_hits["PEOPLE"], locked_hits["LOCATIONS"], rough_list)
216
+ headline, title = suggest_headline_title(locked_hits["LOCATIONS"], rough_list)
217
+ description = suggest_description(locked_hits["LOCATIONS"], rough_list)
218
+ people_variants = people_variants_stub(locked_hits["PEOPLE"]) # replace w/ Gemma later
219
+
220
+ # [7] Summary line
221
+ summary = (
222
+ f"Job {job_number or '(none)'} | " # Job Number
223
+ f"Catalog {neg_number or '(none)'} | " # Neg Number
224
+ f"Total input: {len(rough_list)} | " # Total Keywords Checked
225
+ f"Locked: {sum(len(v) for v in locked_hits.values())} | " # Total Locked Hits Category
226
+ f"In List: {len(in_master)} | " # Total from keyword txt file
227
+ f"New: {len(new_terms)}" # Total new keywords
228
+ )
229
+
230
+ # [8] Return in same order as UI outputs
231
+ return (
232
+ job_number, # Job Number (echo)
233
+ neg_number, # Neg Number (echo)
234
+ "\n".join(slugs), # Recommended filename slugs
235
+ headline, # Headline
236
+ title, # Title
237
+ description, # Description / Alt Text
238
+ locked_block["PEOPLE"], # PEOPLE (Persons Shown)
239
+ people_variants, # PEOPLE VARIANTS (suggested / LLM)
240
+ locked_block["LOCATIONS"], # LOCKED: LOCATIONS
241
+ locked_block["LOCKED"], # LOCKED: OTHER
242
+ in_list_block, # BATCH + SEO
243
+ new_block, # OPTIONAL + SEO
244
+ combined_csv, # All Combined CSV
245
+ "", # Credit Line Field (optional inputs)
246
+ "", # Creator Field (optional inputs)
247
+ summary, # (optional) Summary – nice to have last
248
+ )
249
+
250
+ # =======================================================================================================================================
251
+ # GRADIO GUI CONFIG
252
+ # =======================================================================================================================================
253
+
254
+ # Theme Object
255
+ custom = gr.themes.Base(
256
+ primary_hue="red", # Red main accent color family
257
+ neutral_hue="zinc", # Neutral palette
258
+ ).set( # Override CSS
259
+ # Page text
260
+ body_text_color="#1c1c1c", # black text
261
+ body_text_color_subdued="#666666", # subdued text dark gray
262
+ body_text_size="14px", # Font size
263
+ body_text_weight="400", # regular weight font
264
+
265
+ # Page & blocks
266
+ background_fill_primary="#ffffff", # white cards
267
+ background_fill_secondary="#ffffff", # dark mode, white cards
268
+ block_background_fill="#eeeeee", # light gray background
269
+ block_border_color="#d6d6d6", # light gray border
270
+ block_title_text_color="#1c1c1c", # black text block titles
271
+
272
+ # Inputs/buttons
273
+ input_background_fill="#ffffff", # white input boxes
274
+ input_border_color="#d6d6d6", # light gray border
275
+
276
+ button_primary_background_fill="#ce191c", # red primary buttons
277
+ button_primary_background_fill_hover="#7b2325", # dark red on hover
278
+ button_primary_text_color="#ffffff", # white text on red button
279
+
280
+ button_secondary_background_fill="#343434", # dark mode, very dark gray
281
+ button_secondary_background_fill_hover="#2b2b2b", # dark mode, darker gray on hover
282
+ button_secondary_text_color="#ffffff", # dark mode, white text
283
+
284
+ # Links + radius/shadow
285
+ link_text_color="#1c1c1c", # link text is black
286
+ embed_radius="8px", # all cards/blocks have slightly rounded corners
287
+ border_color_accent="#d6d6d6", # light gray accent borders
288
+ shadow_drop="0 4px 16px rgba(0,0,0,0.08)", # subtle shadow effect around cards
289
+ )
290
+
291
+ CSS = '''
292
+
293
+ /* ====== Palette - unused at the moment, save for later ====== */
294
+ :root{
295
+ --smu-dark-gray: #666666;
296
+ --smu-very-dark: #343434;
297
+ --smu-light-gray: #d6d6d6;
298
+ --smu-off-white: #eeeeee;
299
+ --smu-white: #ffffff;
300
+ --smu-black: #1c1c1c;
301
+ --smu-red: #8e2a2c;
302
+ --smu-salmon: #ff756d; /* reserved for "suggested keywords" – not used yet */
303
+ }
304
+
305
+ /* ====== Hard-lock app to light mode ====== */
306
+ html, :root, body, .gradio-container{
307
+ color-scheme: light !important; /* browsers render controls as light */
308
+ background: var(--smu-off-white) !important; /* page background */
309
+ color: var(--smu-black) !important;
310
+ }
311
+
312
+ /* ====== Global text ====== */
313
+ h1, h2, h3, label, p, .gr-markdown *{
314
+ color: var(--smu-black) !important;
315
+ }
316
+
317
+ /* ====== Panels / cards ====== */
318
+ .gr-block{
319
+ background: var(--smu-white) !important;
320
+ border: 1px solid var(--smu-light-gray) !important;
321
+ border-radius: 8px !important;
322
+ }
323
+
324
+ /* ====== Tabs / top bars (FileMaker-ish chrome) ====== */
325
+ .gr-tabs, .gr-panel{ background: var(--smu-dark-gray) !important; }
326
+ .gr-tabs *, .gr-panel *{ color: var(--smu-white) !important; }
327
+
328
+ /* ====== Inputs ====== */
329
+ .gradio-container input[type="text"],
330
+ .gradio-container textarea{
331
+ background: var(--smu-white) !important;
332
+ color: var(--smu-black) !important;
333
+ border-color: var(--smu-light-gray) !important;
334
+ }
335
+ .gradio-container input[type="text"]::placeholder,
336
+ .gradio-container textarea::placeholder{
337
+ color: var(--smu-dark-gray) !important;
338
+ opacity: 1 !important;
339
+ }
340
+
341
+ /* ====== Buttons ====== */
342
+ button.gr-button-primary{
343
+ background: var(--smu-red) !important; color: #fff !important;
344
+ }
345
+ button.gr-button-primary:hover{ filter: brightness(.92); }
346
+ button.gr-button-secondary{
347
+ background: var(--smu-very-dark) !important; color: #fff !important;
348
+ }
349
+ button.gr-button-secondary:hover{ filter: brightness(1.05); }
350
+
351
+ /* ====== Copy blocks (make borders more visible) ====== */
352
+ /* Add elem_classes=["copyblock"] to those Textboxes you want highlighted */
353
+ .copyblock textarea{
354
+ border: 2px solid var(--smu-dark-gray) !important;
355
+ box-shadow: inset 0 1px 0 rgba(0,0,0,.03);
356
+ }
357
+
358
+ /* ====== Upload zones / dividers ====== */
359
+ #upload_panel, #upload_group{ background: var(--smu-white) !important; border-radius: 8px; }
360
+ input[type="file"], .gr-file{ border-color: var(--smu-light-gray) !important; }
361
+ hr{ border-color: var(--smu-light-gray) !important; }
362
+
363
+ /* ====== Reserved style for future "suggested keywords" (not applied yet) ====== */
364
+ /* Later you can add elem_classes=["suggested"] to a Textbox or container */
365
+ .suggested textarea, .suggested{
366
+ /* NOT enabled yet; uncomment when wiring the feature:
367
+ background: #ff756d1a !important; /* subtle salmon tint */
368
+ border-color: var(--smu-salmon) !important;
369
+ */
370
+ }
371
+ '''
372
+
373
+ # =======================================================================================================================================
374
+ # GRADIO APP
375
+ # =======================================================================================================================================
376
+
377
+
378
+ with gr.Blocks(title="Job Sheet Keyword and Metadata SEO", theme=custom) as demo: # css=CSS is not being used at the moment! # gr.Blocks is a container laying out multiple UI elements
379
+ gr.Markdown( # gr.Markdown block to explain to the user what the app does
380
+ "# Job Sheet Keyword and Metadata SEO \n"
381
+ "Paste rough draft keywords, add optional **locked terms** (PEOPLE / LOCATIONS / NAMES), "
382
+ "and upload the **latest keyword file** (.txt)."
383
+ )
384
+
385
+ with gr.Row(): # gr.Row horizontal components
386
+ job_number = gr.Textbox(label="Job Number", placeholder="i.e. 26-999", scale=1) # Job Number Textbox
387
+ neg_number = gr.Textbox(label="Catalog / Neg Number", placeholder="i.e. 12345D", scale=1) # Neg Number Textbox
388
+ rough = gr.Textbox( # Rough Draft Keywords Textbox
389
+ label="Rough Draft Keywords (comma / newline / semicolon separated)",
390
+ lines=6,
391
+ placeholder="i.e. Owen Arts Center, Meadows School of the Arts, SMU, opening ceremony, ribbon cutting, Dallas, Texas"
392
+ )
393
+
394
+ with gr.Accordion("Locked Terms (will not generate SEO suggestions / synonyms)", open=False, elem_id="locked_panel"): # Dropdown Section Locked Terms
395
+ with gr.Row():
396
+ people = gr.Textbox(label="PEOPLE (Persons Shown)", lines=4, placeholder="i.e. R. Gerald Turner, Samuel S. Holland") # Persons Shown Textbox
397
+ locations = gr.Textbox(label="SUBLOCATION or ADDRESS / CITY / STATE / COUNTRY", lines=4, placeholder="i.e. Owen Arts Center, Dallas, Texas, United States") # Locations Textbox
398
+ names = gr.Textbox(label="OTHER LOCKED TERMS", lines=4, placeholder="i.e. Meadows School of the Arts, Lyle School of Engineering") # Locked Terms Textbox
399
+
400
+ with gr.Row():
401
+ credit_in = gr.Textbox(label="Credit Line (optional input)", placeholder="Southern Methodist University / John Doe", scale=1)
402
+ creator_in = gr.Textbox(label="Creator (optional input)", placeholder="John Doe", scale=1)
403
+
404
+ master = gr.File(label="Upload the latest keyword list (.txt Keyword file; newline or comma separated)", # Upload dialog
405
+ file_types=[".txt"], elem_id="upload_panel")
406
+
407
+ run = gr.Button("Run Keyword Optimizer", variant="primary") # Run Red Button
408
+
409
+ # ---- OUTPUTS (order must match function returns) ----
410
+ job_out = gr.Textbox(label="Job Number", interactive=False, lines=1, elem_classes=["copyblock"])
411
+ neg_out = gr.Textbox(label="Neg Number (Job Identifier Field)", interactive=False, lines=1, elem_classes=["copyblock"])
412
+ slugs_out = gr.Textbox(label="Recommended filename slugs (suggested)", interactive=False, lines=4, elem_classes=["copyblock"])
413
+ headline_out = gr.Textbox(label="Headline", interactive=False, lines=2, elem_classes=["copyblock"])
414
+ title_out = gr.Textbox(label="Title", interactive=False, lines=2, elem_classes=["copyblock"])
415
+ desc_out = gr.Textbox(label="Description / Alt Text", interactive=False, lines=4, elem_classes=["copyblock"])
416
+
417
+ people_out = gr.Textbox(label="PEOPLE (Persons Shown Field)", interactive=False, lines=3, elem_classes=["copyblock"])
418
+ peoplevar_out = gr.Textbox(label="PEOPLE VARIANTS (suggested)", interactive=False, lines=2, elem_classes=["copyblock"])
419
+
420
+ loc_out = gr.Textbox(label="LOCKED: LOCATIONS (copy block)", interactive=False, lines=2, elem_classes=["copyblock"])
421
+ locked_out = gr.Textbox(label="LOCKED TERMS (no synonyms)", interactive=False, lines=2, elem_classes=["copyblock"])
422
+
423
+ in_list_out = gr.Textbox(label="BATCH + SEO (suggested)", interactive=False, lines=4, elem_classes=["copyblock"])
424
+ new_out = gr.Textbox(label="OPTIONAL + SEO (new)", interactive=False, lines=4, elem_classes=["copyblock"])
425
+ csv_out = gr.Textbox(label="All Combined (CSV)", interactive=False, lines=3, elem_classes=["copyblock"])
426
+
427
+ credit_out = gr.Textbox(label="Credit Line Field", interactive=False, lines=1, elem_classes=["copyblock"])
428
+ creator_out = gr.Textbox(label="Creator Field", interactive=False, lines=1, elem_classes=["copyblock"])
429
+
430
+ summary_out = gr.Textbox(label="Summary (debug)", interactive=False, lines=2)
431
+
432
+ # Click wiring – note input list includes credit/creator, pass them into function if you want to use them
433
+ run.click(
434
+ fn=keyword_checker,
435
+ inputs=[job_number, neg_number, rough, people, locations, names, master],
436
+ outputs=[job_out, neg_out, slugs_out, headline_out, title_out, desc_out,
437
+ people_out, peoplevar_out, loc_out, locked_out,
438
+ in_list_out, new_out, csv_out, credit_out, creator_out, summary_out]
439
+ )
440
+
441
+
442
+ #demo.launch(share=True) # launches the app, switch to false to turn off sharing
443
+
444
+ if __name__ == "__main__":
445
+ demo.launch() # local runs only
readme.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SMU Keyword Optimizer
3
+ emoji: 🐴
4
+ colorFrom: red
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ python_version: 3.11
9
+ app_file: app.py
10
+ pinned: false
11
+ ---
12
+
13
+ # SMU Keyword Optimizer (Proof of Concept)
14
+
15
+ Gradio POC for structured IPTC blocks and keyword QA.
16
+ **How to run locally:**
17
+ ```bash
18
+ pip install -r requirements.txt
19
+ python app.py
20
+
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # required
2
+ gradio
3
+
4
+ # optional (uncomment later)
5
+ # requests
6
+ # pandas
7
+ # python-docx
8
+ # pdfplumber
9
+ # nltk
10
+ # wordfreq
11
+ # symspellpy
12
+ # datamuse
13
+ # pytrends