File size: 8,663 Bytes
a293444
 
 
ef8dd8b
a293444
 
 
 
 
 
 
 
ef8dd8b
a293444
 
ef8dd8b
 
a293444
 
ef8dd8b
 
 
a293444
 
ef8dd8b
a293444
ef8dd8b
 
a293444
ef8dd8b
a293444
ef8dd8b
 
 
a293444
 
b084b77
 
 
 
 
 
 
 
 
 
 
ef8dd8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a293444
 
 
 
ef8dd8b
 
a293444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef8dd8b
 
a293444
ef8dd8b
 
 
a293444
ef8dd8b
 
a293444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef8dd8b
 
 
a293444
ef8dd8b
 
 
a293444
 
ef8dd8b
 
 
 
 
 
 
 
 
a293444
ef8dd8b
 
 
 
a293444
ef8dd8b
 
 
 
 
a293444
ef8dd8b
 
 
 
a293444
 
b084b77
 
 
 
a293444
b084b77
 
 
a293444
b084b77
 
 
d2a7e23
b084b77
d2a7e23
b084b77
 
 
 
 
 
 
 
a293444
b084b77
 
 
a293444
b084b77
 
a293444
b084b77
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import json
import os
import random
from typing import List, Tuple

import google.generativeai as genai
import gradio as gr
from jinja2 import Environment, FileSystemLoader

GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
genai.configure(api_key=GOOGLE_API_KEY)

MODEL = genai.GenerativeModel("gemini-1.5-flash-latest",
                              generation_config={"response_mime_type": "application/json"})

# Example clues and groupings
EXAMPLE_CLUES = [
    (['ARROW', 'TIE', 'HONOR'], 'BOW', 'such as a bow and arrow, a bow tie, or a bow as a sign of honor'),
    (['DOG', 'TREE'], 'BARK', 'such as the sound a dog makes, or a tree is made of bark'),
    (['MONEY', 'RIVER', 'ROB', 'BLOOD'], 'CRIME', 'such as money being stolen, a river being a potential crime scene, robbery, or blood being a result of a violent crime'),
    (['BEEF', 'TURKEY', 'FIELD', 'GRASS'], 'GROUND', 'such as ground beef, a turkey being a ground-dwelling bird, a field or grass being a type of ground'),
    (['BANK', 'GUITAR', 'LIBRARY'], 'NOTE', 'such as a bank note, a musical note on a guitar, or a note being a written comment in a library book'),
    (['ROOM', 'PIANO', 'TYPEWRITER'], 'KEYS', 'such as a room key, piano keys, or typewriter keys'),
    (['TRAFFIC', 'RADAR', 'PHONE'], 'SIGNAL', 'such as traffic signals, radar signals, or phone signals'),
    (['FENCE', 'PICTURE', 'COOKIE'], 'FRAME', 'such as a frame around a yard, a picture frame, or a cookie cutter being a type of frame'),
    (['YARN', 'VIOLIN', 'DRESS'], 'STRING', 'strings like material, instrument, clothing fastener'),
    (['JUMP', 'FLOWER', 'CLOCK'], 'SPRING', 'such as jumping, flowers blooming in the spring, or a clock having a sprint component'),
    (['SPY', 'KNIFE'], 'WAR', 'Both relate to aspects of war, such as spies being involved in war or knives being used as weapons'),
    (['STADIUM', 'SHOE', 'FIELD'], 'SPORT', 'Sports like venues, equipment, playing surfaces'),
    (['TEACHER', 'CLUB'], 'SCHOOL', 'such as a teacher being a school staff member or a club being a type of school organization'),
    (['CYCLE', 'ARMY', 'COURT', 'FEES'], 'CHARGE', 'charges like electricity, battle, legal, payments'),
    (['FRUIT', 'MUSIC', 'TRAFFIC', 'STUCK'], 'JAM', 'Jams such as fruit jam, a music jam session, traffic jam, or being stuck in a jam'),
    (['POLICE', 'DOG', 'THIEF'], 'CRIME', 'such as police investigating crimes, dogs being used to detect crimes, or a thief committing a crime'),
    (['ARCTIC', 'SHUT', 'STAMP'], 'SEAL', 'such as the Arctic being home to seals, or shutting a seal on an envelope, or a stamp being a type of seal'),
]

def create_random_word_groups(clues: List[Tuple[List[str], str, str]], num_groups: int = 10) -> List[Tuple[List[str], List[int]]]:
    """Creates random groups of words from the given clues."""
    word_groups = []
    while len(word_groups) < num_groups:
        group_size = random.choice([3, 4])
        selected_indices = random.sample(range(len(clues)), group_size)
        words = [word for row in [clues[i][0] for i in selected_indices] for word in row]
        if len(words) in [8, 9]:
            word_groups.append((words, selected_indices))
    return word_groups

def create_example_groupings(clues: List[Tuple[List[str], str, str]], num_groups: int = 5) -> List[Tuple[List[str], str]]:
    """Creates example groupings from the given clues."""
    merged = create_random_word_groups(clues, num_groups)
    return [
        (
            merged_words,
            json.dumps([{
                "words": clues[i][0],
                "clue": clues[i][1],
                "explanation": clues[i][2]
            } for i in indices], separators=(',', ':'))
        )
        for merged_words, indices in merged
    ]

EXAMPLE_GROUPINGS = create_example_groupings(EXAMPLE_CLUES)

def render_template(template: str, system_prompt: str, history: List[Tuple], query: str) -> str:
    """Renders a Jinja2 template with the given parameters."""
    env = Environment(loader=FileSystemLoader('.'))
    template = env.from_string(template)
    return template.render(system_prompt=system_prompt, history=history, query=query)

def group_words(words: List[str]) -> List[dict]:
    """Groups the given words using the AI model."""
    template = '''
    {% for example in history %}
    INPUT:
    {{ example[0] }}
    OUTPUT:
    {{ example[1] }}
    {% endfor %}
    INPUT:
    {{ query }}
    OUTPUT:

    {{ system }}

    Groups = {'words': list[str], 'clue': str, 'explanation': str}
    Return: Groups
    '''

    grouping_prompt = ("You are an assistant for the game Codenames. Group the given words into 3 to 4 sets of 2 to 4 words each. "
                       "Each group should share a common theme or word connection. Avoid generic or easily guessable clues.")

    prompt = render_template(template, grouping_prompt, EXAMPLE_GROUPINGS, words)
    response = MODEL.generate_content(prompt, generation_config={'top_k': 3, 'temperature': 1.1})
    return json.loads(response.text)

def generate_clue(group: List[str]) -> dict:
    """Generates a clue for the given group of words using the AI model."""
    template = '''
    {% for example in history %}
    INPUT:
    {{ example[0] }}
    OUTPUT:
    { 'clue':{{ example[1] }}, 'explanation':{{ example[2] }} }
    {% endfor %}
    INPUT:
    {{ query }}
    OUTPUT:

    {{ system }}

    Clue = {'clue': str, 'explanation': str}
    Return: Clue
    '''

    clue_prompt = ("As a Codenames game companion, provide a single-word clue for the given group of words. "
                   "The clue should relate to a common theme or word connection. DO NOT reuse any of the given "
                   "words as a clue. Avoid generic or easily guessable clues.")

    prompt = render_template(template, clue_prompt, EXAMPLE_CLUES, group)
    response = MODEL.generate_content(prompt, generation_config={'top_k': 3, 'temperature': 1.1})
    return json.loads(response.text)


def process_image(img) -> gr.update:
    """Processes the uploaded image and extracts words for the game."""
    prompt = ('Identify the words in this Codenames game image. Provide only a list of words in capital letters. '
              'Group these words into 6 or 8 sets that can be guessed together using a single-word clue. '
              'Respond with JSON in the format: {"Game": <list of words in the game>}')
    response = MODEL.generate_content([prompt, img], stream=True)
    response.resolve()
    words = json.loads(response.text)['Game']
    return gr.update(choices=words, value=words)

def pad_or_truncate(lst: List, n: int = 4) -> List:
    """Ensures the list has exactly n elements, padding with None if necessary."""
    truncated_lst = lst[:n]
    return truncated_lst + (n - len(truncated_lst)) * [{}]

def group_words_callback(words: List[str]) -> List[gr.update]:
    """Callback function for grouping words."""
    groups = group_words(words)
    groups = pad_or_truncate(groups, 4)
    return [gr.update(value=groups[i].get("words", ""), choices=words, info=groups[i].get("explanation","")) for i in range(4)]

def generate_clue_callback(group: List[str]) -> gr.update:
    """Callback function for generating clues."""
    clue = generate_clue(group)
    return gr.update(value=clue['clue'], info=clue['explanation'])


# UI Setup
with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
    gr.Markdown("# *Codenames* Clue Generator")
    gr.Markdown("Provide a list of words to generate clues")

    with gr.Row():
        game_image = gr.Image(type="pil")
        word_list_input = gr.Dropdown(label="Detected words", choices=[], multiselect=True, interactive=True)

    with gr.Row():
        detect_words_button = gr.Button("Detect Words")
        group_words_button = gr.Button("Group Words")

    group_inputs, clue_buttons, clue_outputs = [], [], []

    for i in range(4):
        with gr.Row():
            group_input = gr.Dropdown(label=f"Group {i + 1}", choices=[], allow_custom_value=True, multiselect=True, interactive=True)
            clue_button = gr.Button("Generate Clue", size='sm')
            clue_output = gr.Textbox(label=f"Clue {i + 1}")
            group_inputs.append(group_input)
            clue_buttons.append(clue_button)
            clue_outputs.append(clue_output)

    # Event handlers
    detect_words_button.click(fn=process_image, inputs=game_image, outputs=[word_list_input])
    group_words_button.click(fn=group_words_callback, inputs=word_list_input, outputs=group_inputs)

    for i in range(4):
        clue_buttons[i].click(generate_clue_callback, inputs=group_inputs[i], outputs=clue_outputs[i])

demo.launch(share=True, debug=True)