import gradio as gr
import re
import pandas as pd
def build_keywords_dict(primary_inputs, synonym_inputs):
"""Build keyword dictionary from separate primary and synonym inputs"""
keywords_dict = {}
for primary, synonyms in zip(primary_inputs, synonym_inputs):
if primary and primary.strip(): # Only process if primary keyword exists
primary_clean = primary.strip()
if synonyms and synonyms.strip():
synonym_list = [s.strip() for s in synonyms.split(';') if s.strip()]
else:
synonym_list = []
keywords_dict[primary_clean] = synonym_list
return keywords_dict
def find_keywords(story, keywords_dict):
"""Find keywords in the story text"""
if not story or not isinstance(story, str):
return ''
found_keywords = set()
# Search for each primary keyword and its synonyms
for primary_keyword, synonyms in keywords_dict.items():
keyword_group_found = False
# Check primary keyword
if primary_keyword.upper() == "US":
if ' US ' in story or story.startswith('US ') or story.endswith(' US'):
keyword_group_found = True
else:
pattern = r'\b' + re.escape(primary_keyword) + r'\b'
if re.search(pattern, story, re.IGNORECASE):
keyword_group_found = True
# Check each synonym
for synonym in synonyms:
if synonym.upper() == "US":
if ' US ' in story or story.startswith('US ') or story.endswith(' US'):
keyword_group_found = True
else:
if re.search(r'\b' + re.escape(synonym) + r'\b', story, re.IGNORECASE):
keyword_group_found = True
# If any keyword from this group was found, add ALL keywords from the group
if keyword_group_found:
found_keywords.add(primary_keyword) # Always include the primary
found_keywords.update(synonyms) # Add all synonyms
return '; '.join(sorted(found_keywords))
def highlight_keywords_in_text(text, keywords_list):
"""Create HTML with highlighted keywords while preserving line breaks"""
if not keywords_list:
# Convert line breaks to HTML breaks for plain text
formatted_text = text.replace('\n', '
')
return formatted_text
highlighted_text = text
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#F9CA24', '#6C5CE7', '#A0E7E5', '#FD79A8', '#55A3FF', '#00B894', '#E17055']
for i, keyword in enumerate(keywords_list):
if keyword:
color = colors[i % len(colors)]
pattern = r'\b' + re.escape(keyword) + r'\b'
replacement = f'{keyword}'
highlighted_text = re.sub(pattern, replacement, highlighted_text, flags=re.IGNORECASE)
# Convert line breaks to HTML breaks after highlighting
highlighted_text = highlighted_text.replace('\n', '
')
return highlighted_text
def create_keyword_results_table(found_keywords_str, keywords_dict, input_text):
"""Create HTML table showing detailed keyword results"""
if not found_keywords_str:
return "
No keywords found.
"
found_keywords = found_keywords_str.split('; ')
# Group keywords by their primary category
keyword_groups = {}
for primary, synonyms in keywords_dict.items():
found_in_group = []
# Check if primary keyword was found
if primary in found_keywords:
found_in_group.append(primary)
# Check if any synonyms were found
for synonym in synonyms:
if synonym in found_keywords:
found_in_group.append(synonym)
if found_in_group:
keyword_groups[primary] = found_in_group
if not keyword_groups:
return "No keyword groups matched.
"
# Create the HTML table
table_html = """
📊 Detailed Keyword Results
| Primary Keyword |
Found Terms |
Count in Text |
Context Preview |
"""
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#F9CA24', '#6C5CE7', '#A0E7E5', '#FD79A8', '#55A3FF', '#00B894', '#E17055']
for i, (primary, found_terms) in enumerate(keyword_groups.items()):
color = colors[i % len(colors)]
# Count total occurrences and get context
total_count = 0
contexts = []
for term in found_terms:
# Count occurrences (case insensitive)
if term.upper() == "US":
# Special handling for "US"
count = len([m for m in re.finditer(r'\bUS\b', input_text)])
else:
pattern = r'\b' + re.escape(term) + r'\b'
count = len(list(re.finditer(pattern, input_text, re.IGNORECASE)))
total_count += count
# Get context (first occurrence)
if term.upper() == "US":
match = re.search(r'\bUS\b', input_text)
else:
match = re.search(r'\b' + re.escape(term) + r'\b', input_text, re.IGNORECASE)
if match:
start = max(0, match.start() - 30)
end = min(len(input_text), match.end() + 30)
context = input_text[start:end].replace('\n', ' ')
# Highlight the found term in context
if term.upper() == "US":
highlighted_context = re.sub(
r'\bUS\b',
f'{term}',
context
)
else:
highlighted_context = re.sub(
r'\b' + re.escape(term) + r'\b',
f'{term}',
context,
flags=re.IGNORECASE
)
contexts.append(highlighted_context)
# Create found terms display
found_terms_display = []
for term in found_terms:
found_terms_display.append(f'{term}')
table_html += f"""
| {primary} |
{' '.join(found_terms_display)} |
{total_count}
|
{contexts[0] if contexts else 'No context available'}...
|
"""
table_html += """
"""
return table_html
def process_text(input_text, primary1, synonyms1, primary2, synonyms2, primary3, synonyms3, primary4, synonyms4, primary5, synonyms5):
"""Main processing function with added results table"""
if not input_text.strip():
return "Please enter some text to analyse", "", "", "No keywords found"
# Build keywords dictionary from separate inputs
primary_inputs = [primary1, primary2, primary3, primary4, primary5]
synonym_inputs = [synonyms1, synonyms2, synonyms3, synonyms4, synonyms5]
keywords_dict = build_keywords_dict(primary_inputs, synonym_inputs)
if not keywords_dict:
return "Please enter at least one primary keyword", "", "", "No keyword dictionary provided"
# Find keywords in the text
found_keywords_str = find_keywords(input_text, keywords_dict)
if not found_keywords_str:
return f"No keywords found in the text.\n\nKeyword dictionary loaded: {len(keywords_dict)} primary keywords", input_text, "", "No matches found"
# Create highlighted version
keywords_list = found_keywords_str.split('; ')
highlighted_html = highlight_keywords_in_text(input_text, keywords_list)
# Create results table
results_table_html = create_keyword_results_table(found_keywords_str, keywords_dict, input_text)
# Create results summary
results_summary = f"""
## Results Summary
**Keywords Found:** {len(keywords_list)}
**Matched Keywords:** {found_keywords_str}
**Keyword Dictionary Stats:**
- Primary keywords loaded: {len(keywords_dict)}
- Total searchable terms: {sum(len(synonyms) + 1 for synonyms in keywords_dict.values())}
**Copy this result to your spreadsheet:**
{found_keywords_str}
"""
return results_summary, highlighted_html, results_table_html, found_keywords_str
# Create the Gradio interface
def create_interface():
with gr.Blocks(title="Keyword Tagging Tool", theme=gr.themes.Soft()) as demo:
gr.HTML("""
Controlled Vocabluary Keyword Tagging Tool
This tool demonstrates how a simple python script can be used to extract keywords from text using a controlled vocabulary of primary keywords and associated keywords/synonyms.
How to use this tool:
- 📝 Enter your text in the left panel
- 📚 Define your keyword dictionary in the right panel - enter primary keywords and their synonyms
- 🔍 Click "Find Keywords" to see results
- 📋 Copy the results to paste into your spreadsheet
""")
with gr.Row():
with gr.Column(scale=1):
text_input = gr.Textbox(
label="Text to Analyse",
placeholder="Enter the text you want to tag with keywords...",
lines=22,
max_lines=25
)
with gr.Column(scale=1):
gr.Markdown("**Keyword Dictionary** - Enter primary keywords and their synonyms:")
# Row 1
with gr.Row():
primary1 = gr.Textbox(label="Primary Keyword 1", placeholder="e.g., Prisoner of War", scale=1)
synonyms1 = gr.Textbox(label="Synonyms 1", placeholder="e.g., POW; POWs; prisoner of war", scale=2)
# Row 2
with gr.Row():
primary2 = gr.Textbox(label="Primary Keyword 2", placeholder="e.g., United States", scale=1)
synonyms2 = gr.Textbox(label="Synonyms 2", placeholder="e.g., USA; US; America", scale=2)
# Row 3
with gr.Row():
primary3 = gr.Textbox(label="Primary Keyword 3", placeholder="e.g., University", scale=1)
synonyms3 = gr.Textbox(label="Synonyms 3", placeholder="e.g., university; institution; college", scale=2)
# Row 4
with gr.Row():
primary4 = gr.Textbox(label="Primary Keyword 4", placeholder="Optional", scale=1)
synonyms4 = gr.Textbox(label="Synonyms 4", placeholder="Optional", scale=2)
# Row 5
with gr.Row():
primary5 = gr.Textbox(label="Primary Keyword 5", placeholder="Optional", scale=1)
synonyms5 = gr.Textbox(label="Synonyms 5", placeholder="Optional", scale=2)
# Full width Find Keywords button
with gr.Row():
find_btn = gr.Button("Find Keywords", variant="primary", size="lg")
# Clear buttons side by side under Find Keywords
with gr.Row():
clear_text_btn = gr.Button("Clear Text", size="lg", variant="secondary")
clear_dict_btn = gr.Button("Clear Dictionary", size="lg", variant="secondary")
# Horizontal line before results
gr.HTML("
")
with gr.Row():
results_output = gr.Markdown(label="Results Summary")
with gr.Row():
highlighted_output = gr.HTML(label="Text with Highlighted Keywords")
with gr.Row():
results_table_output = gr.HTML(label="Detailed Results Table")
with gr.Row():
copy_output = gr.Textbox(
label="Keywords for Spreadsheet (copy this text)",
lines=3,
max_lines=5
)
# Examples section with table format
gr.Markdown("### Examples")
example1 = [
"During World War II, many prisoners of war were held in camps across Europe. The Geneva Convention established rules for POW treatment. American soldiers and British troops were among those captured.",
"Prisoner of War", "POW; POWs; prisoner of war",
"World War II", "WWII; Second World War",
"United States", "USA; US; America; American",
"", "", "", ""
]
example2 = [
"The University of Oxford is located in Oxford, England. Students from around the world study at this prestigious institution.",
"University", "university; institution; college",
"Oxford", "oxford",
"England", "england; English",
"Student", "student; students; pupils",
"", ""
]
gr.Examples(
examples=[example1, example2],
inputs=[text_input, primary1, synonyms1, primary2, synonyms2, primary3, synonyms3, primary4, synonyms4, primary5, synonyms5],
label="Click an example to try it out"
)
# Clear functions
def clear_text_only():
"""Clear only the text input field"""
return ""
def clear_dictionary_only():
"""Clear only the keyword dictionary fields"""
return "", "", "", "", "", "", "", "", "", ""
# Button functions
find_btn.click(
fn=process_text,
inputs=[text_input, primary1, synonyms1, primary2, synonyms2, primary3, synonyms3, primary4, synonyms4, primary5, synonyms5],
outputs=[results_output, highlighted_output, results_table_output, copy_output]
)
clear_text_btn.click(
fn=clear_text_only,
outputs=[text_input]
)
clear_dict_btn.click(
fn=clear_dictionary_only,
outputs=[primary1, synonyms1, primary2, synonyms2, primary3, synonyms3, primary4, synonyms4, primary5, synonyms5]
)
# Instructions
gr.Markdown("""
## Format Guide
**How to enter keywords:**
- **Primary Keyword:** Enter the main/preferred term for a concept
- **Synonyms:** Enter alternative terms separated by semicolons `;`
- Leave rows blank if you don't need all 5 keyword groups
- The tool will find ANY of these terms and return ALL related terms
**Example:**
- Primary: `Prisoner of War`
- Synonyms: `POW; POWs; prisoner of war`
**Special Handling:**
- "US" is matched exactly to avoid confusion with the word "us"
- Word boundaries are respected (prevents partial matches)
- Results are alphabetised and deduplicated
**How it works:**
When ANY variant is found in your text (primary OR synonym), the tool returns the complete standardized set of terms for that concept.
""")
# Bottom horizontal line and footer
gr.HTML("
")
gr.HTML("""
The code for this tool was built with the aid of Claude Sonnet 4.
""")
return demo
if __name__ == "__main__":
demo = create_interface()
demo.launch()