# ============================================================ # Helpers # ============================================================ import io, base64, re, json, os from PIL import Image from rembg import remove from pathlib import Path from huggingface_hub import InferenceClient EMOJI_TAG_PATTERN = re.compile(r"(.*?)", re.DOTALL) THINK_PATTERN = re.compile(r".*?", re.DOTALL) TOOL_CALL_PATTERN = re.compile(r"\s*(.+?)\s*", re.DOTALL) CUSTOM_CSS = Path("style.css").read_text() PAGE_JS_BODY = Path("script.js").read_text() PAGE_JS = f"() => {{\n{PAGE_JS_BODY}\n}}" INFO_MD = Path("info.md").read_text() HF_TOKEN = os.environ.get("HF_TOKEN") client = InferenceClient(token=HF_TOKEN) QWEN_MODEL = "Qwen/Qwen3-8B" FLUX_MODEL = "black-forest-labs/FLUX.1-schnell" def generate_emoji_image(description: str) -> Image.Image: prompt = ( "Apple iPhone emoji style, clean flat vector emoji illustration, " "single subject only, large subject filling 80-90% of the frame, " "tight crop, minimal padding, centered composition, " "no extra background elements, no text, no border, " "white background, high contrast, emoji-sized readability: " f"{description}" ) image = client.text_to_image( prompt=prompt, model=FLUX_MODEL, width=512, height=512, ) # client.text_to_image returns a PIL Image directly image = image.convert("RGBA") image = remove(image) return image def image_to_b64(image: Image.Image) -> str: buf = io.BytesIO() image.save(buf, format="PNG") return base64.b64encode(buf.getvalue()).decode() def render_emojis(text: str, inventory: list) -> str: emoji_map = { item["name"]: item["image_b64"] for item in inventory if "name" in item and "image_b64" in item } def replace(match): name = match.group(1).strip() if name in emoji_map: b64 = emoji_map[name] return ( f'' ) return "" result = EMOJI_TAG_PATTERN.sub(replace, text) result = re.sub(r"[ \t]+", " ", result).strip() return result # ============================================================ # Tool schema # ============================================================ GENERATE_EMOJI_TOOL = { "type": "function", "function": { "name": "generate_emoji", "description": ( "Generates a brand-new emoji-style image. " "Call this when the user asks for a new emoji, sticker, or icon. " "Provide a short unique snake_case name and a visual description for image generation." ), "parameters": { "type": "object", "properties": { "name": { "type": "string", "description": "Short unique snake_case identifier, e.g. 'pizza_slice' or 'happy_cat'", }, "description": { "type": "string", "description": "Visual description used to generate the image, e.g. 'a steaming pizza slice with melted cheese'", }, }, "required": ["name", "description"], }, }, } # ============================================================ # System prompt # ============================================================ BASE_SYSTEM_PROMPT = """\ You are a helpful assistant in a chat app that supports custom emojis. Custom emojis are inserted with name syntax. When you use a \ registered emoji name inside those tags it renders as an image. RULES: - DEFAULT: use NO custom emoji. This is the default for every message, including greetings, \ small talk, and generic replies. - Do NOT use standard Unicode emoji characters at the same time as custom emojis name. - STRICT MATCH ONLY: insert a custom emoji ONLY if the user's message explicitly expresses or describes \ the exact situation in that emoji's "meaning" (e.g. the user states they feel sad, stressed, down, etc. \ for a "cheer up when sad" emoji). A neutral or generic message (e.g. "hi", "hello", "how are you", \ "ok", "thanks") NEVER qualifies, even if the emoji's meaning is broadly positive or cheerful. - If you are unsure whether a message qualifies, do NOT use the emoji. - Only use names from the "Registered emojis" list. Never guess or invent names. - Never responds with a single emoji alone without additional text. - Never refer to or describe an emoji in text (e.g. "here's a", "have this emoji"). \ Insert emoji only as a decorative inline addition to add meaning. - When the user asks for a new emoji/sticker/icon, call the generate_emoji tool \ with a fitting name and description. Do not ask the user for the name or description. - After the tool call your job is done — the system handles the follow-up flow. """ def build_system_prompt(inventory: list) -> str: registered = [it for it in inventory if "name" in it and "image_b64" in it and it.get("meaning")] if not registered: emoji_section = "(No custom emojis registered yet.)" else: lines = [ f'- name: "{it["name"]}" | meaning: {it["meaning"]}' for it in registered ] emoji_section = "Registered emojis:\n" + "\n".join(lines) return BASE_SYSTEM_PROMPT + "\n" + emoji_section # ============================================================ # LLM helpers # ============================================================ def strip_think(text: str) -> str: return THINK_PATTERN.sub("", text).strip() def run_qwen(messages, tools=None, max_new_tokens=512): # Build the chat_completion call; tools forwarded if provided kwargs = dict( model=QWEN_MODEL, messages=messages, max_tokens=max_new_tokens, temperature=0.2, top_p=0.9, ) if tools: kwargs["tools"] = tools response = client.chat_completion(**kwargs) msg = response.choices[0].message # If the model returned a native tool_call (OpenAI-style), re-serialise # it as the format the rest of the code expects. if msg.tool_calls: tc = msg.tool_calls[0] args = tc.function.arguments if isinstance(args, dict): args = json.dumps(args) payload = json.dumps({"name": tc.function.name, "arguments": json.loads(args)}) return f"{payload}" return (msg.content or "").strip() def _extract_tool_call(reply: str): match = TOOL_CALL_PATTERN.search(reply) if not match: return None, {} try: call = json.loads(match.group(1)) return call.get("name"), call.get("arguments", {}) or {} except json.JSONDecodeError: return None, {} def _strip_tool_artifacts(text: str) -> str: text = TOOL_CALL_PATTERN.sub("", text) text = re.sub(r"", "", text) text = re.sub( r'\{\s*"name"\s*:\s*"generate_emoji".*\}\s*$', "", text, flags=re.DOTALL, ) return text.strip() # ============================================================ # Chat logic # ============================================================ ASKING_MSG = "What should this emoji mean, and when would you use it? (type 'cancel' to abort)" def chat_turn(message: str, llm_history: list, inventory: list, pending_emoji: dict | None): system_prompt = build_system_prompt(inventory) if pending_emoji is not None: meaning = message.strip() # abort emoji generation if meaning.lower().strip() == "cancel": abort_raw = "Got it, emoji generation cancelled. No emoji was saved." assistant_messages = [{"role": "assistant", "content": abort_raw, "raw": abort_raw}] updated_llm_history = llm_history + [ {"role": "user", "content": message}, {"role": "assistant", "content": abort_raw}, ] return assistant_messages, None, inventory, updated_llm_history, None completed_entry = {**pending_emoji, "meaning": meaning} updated_inventory = inventory + [completed_entry] emoji_tag = f"{pending_emoji['name']}" confirm_raw = f"Got it! Your new emoji {emoji_tag} has been saved. You can now use it in any message!" confirm_display = render_emojis(confirm_raw, updated_inventory) assistant_messages = [{"role": "assistant", "content": confirm_display, "raw": confirm_raw}] updated_llm_history = llm_history + [ {"role": "user", "content": message}, {"role": "assistant", "content": confirm_raw}, ] return assistant_messages, None, updated_inventory, updated_llm_history, None messages = [{"role": "system", "content": system_prompt}] + llm_history messages.append({"role": "user", "content": message}) reply = strip_think(run_qwen(messages, tools=[GENERATE_EMOJI_TOOL])) fn_name, fn_args = _extract_tool_call(reply) if fn_name == "generate_emoji": name = (fn_args.get("name") or "").strip() description = (fn_args.get("description") or "").strip() if not name: name = re.sub(r"[^a-z0-9]+", "_", description.lower()).strip("_")[:32] print(f"[Tool] generate_emoji name={name!r} desc={description!r}") image = generate_emoji_image(description) b64 = image_to_b64(image) pending = {"name": name, "description": description, "image_b64": b64} ask_display = render_emojis(ASKING_MSG, inventory) assistant_messages = [ {"role": "assistant", "content": {"image_b64": b64}}, {"role": "assistant", "content": ask_display, "raw": ASKING_MSG}, ] updated_llm_history = llm_history + [ {"role": "user", "content": message}, {"role": "assistant", "content": f"[Generated emoji '{name}'. Awaiting user description.]"}, ] return assistant_messages, image, inventory, updated_llm_history, pending reply = _strip_tool_artifacts(reply) or "Got it!" reply_display = render_emojis(reply, inventory) assistant_messages = [{"role": "assistant", "content": reply_display, "raw": reply}] updated_llm_history = llm_history + [ {"role": "user", "content": message}, {"role": "assistant", "content": reply}, ] return assistant_messages, None, inventory, updated_llm_history, None # ============================================================ # Gradio UI # ============================================================ import gradio as gr # ── Chat HTML renderer ───────────────────────────────────────────────────── def render_chat_html(display_history: list) -> str: rows = [] for msg in display_history: role = msg["role"] content = msg["content"] is_sys = msg.get("system_note", False) is_typing = msg.get("typing", False) if is_typing: rows.append( '
' '
' '' '
' ) elif is_sys: rows.append( '
' f'
{content}
' ) elif isinstance(content, dict) and "image_b64" in content: rows.append( '
' f'
' f'generated emoji' f'
' ) else: css = "u" if role == "user" else "b" rows.append( f'
' f'
{content}
' ) if not rows: body = ( '
' 'Chat or ask for a new emoji!' '
' ) else: body = "".join(rows) return f'
{body}
' # ── Emoji picker inventory sync ──────────────────────────────────────────── def render_picker_sync(inventory: list) -> str: safe_json = json.dumps(inventory).replace('"', """) return f'
' # ── Gradio event handlers ────────────────────────────────────────────────── def _submit_user(message, display_history, llm_history, inventory, pending_emoji): msg = str(message or "").strip() if not msg: return render_chat_html(display_history), display_history, llm_history, inventory, pending_emoji, "" user_display = render_emojis(msg, inventory) new_display = display_history + [ {"role": "user", "content": user_display}, {"role": "assistant", "content": "...", "typing": True}, ] return render_chat_html(new_display), new_display, llm_history, inventory, pending_emoji, msg def _submit_bot(message, display_history, llm_history, inventory, pending_emoji): display_history = [m for m in display_history if not m.get("typing")] msg = str(message or "").strip() if not msg: return ( render_chat_html(display_history), display_history, llm_history, inventory, pending_emoji, render_picker_sync(inventory), ) asst_msgs, new_image, updated_inv, updated_hist, new_pending = chat_turn( msg, llm_history, inventory, pending_emoji ) new_display = list(display_history) if new_image is not None: b64 = image_to_b64(new_image) new_display.append({"role": "assistant", "content": {"image_b64": b64}}) for am in asst_msgs: if isinstance(am["content"], str): new_display.append({"role": "assistant", "content": am["content"]}) return ( render_chat_html(new_display), new_display, updated_hist, updated_inv, new_pending, render_picker_sync(updated_inv), ) def _clear_chat(inventory): return render_chat_html([]), [], [], render_picker_sync(inventory), None # ── Build UI ─────────────────────────────────────────────────────────────── with gr.Blocks(css=CUSTOM_CSS, title="Emoji Studio", analytics_enabled=False) as demo: # States inventory_state = gr.State([]) llm_history_state = gr.State([]) display_history_state = gr.State([]) pending_emoji_state = gr.State(None) msg_state = gr.State("") # ── App shell ── with gr.Column(elem_id="app-shell"): # Header gr.HTML("""
Emoji Studio
Create your own emojies, give them meaning, and use them with your chatbot.
""") with gr.Column(elem_id="info-overlay"): gr.HTML('') gr.Markdown(INFO_MD, elem_id="info-content") # Chat display chat_html = gr.HTML(render_chat_html([]), elem_id="chat-display") # Hidden inventory sync (updates picker JS-side) picker_sync = gr.HTML(render_picker_sync([]), elem_id="picker-sync") # Input zone with gr.Row(elem_id="input-zone"): with gr.Row(elem_id="composer-wrap"): gr.HTML("""
""") txt = gr.Textbox( value="", show_label=False, container=False, lines=1, elem_id="hidden-txt", visible=False, ) # Send/Clear row below, 50/50 with gr.Row(elem_id="bottom-btn-row"): send_btn = gr.Button("Send", elem_id="send-btn", elem_classes=["icon-btn"]) clear_btn = gr.Button("Clear", elem_id="clear-btn", elem_classes=["icon-btn"]) gr.HTML(""" """) # ── Event wiring ── SEND_IN = [txt, display_history_state, llm_history_state, inventory_state, pending_emoji_state] USER_OUT = [chat_html, display_history_state, llm_history_state, inventory_state, pending_emoji_state, msg_state] BOT_IN = [msg_state, display_history_state, llm_history_state, inventory_state, pending_emoji_state] BOT_OUT = [chat_html, display_history_state, llm_history_state, inventory_state, pending_emoji_state, picker_sync] for trigger in [send_btn.click, txt.submit]: trigger( _submit_user, inputs=SEND_IN, outputs=USER_OUT, show_progress="hidden", ).then( lambda: "", outputs=[txt], show_progress="hidden", js=( "() => {" " const c = document.getElementById('composer');" " if (c) { c.innerHTML = ''; c.contentEditable = 'false'; }" " const send = document.getElementById('send-btn');" " const clear = document.getElementById('clear-btn');" " const emoji = document.getElementById('emoji-pick-btn');" " if (send) send.disabled = true;" " if (clear) clear.disabled = true;" " if (emoji) emoji.disabled = true;" " document.getElementById('input-zone').classList.add('disabled');" " if (window.__syncComposer) window.__syncComposer();" " return '';" "}" ), ).then( _submit_bot, inputs=BOT_IN, outputs=BOT_OUT, show_progress="hidden", ).then( None, None, None, show_progress="hidden", js=( "() => {" " const c = document.getElementById('composer');" " if (c) { c.contentEditable = 'true'; c.focus(); }" " const send = document.getElementById('send-btn');" " const clear = document.getElementById('clear-btn');" " const emoji = document.getElementById('emoji-pick-btn');" " if (send) send.disabled = false;" " if (clear) clear.disabled = false;" " if (emoji) emoji.disabled = false;" " document.getElementById('input-zone').classList.remove('disabled');" " return [];" "}" ), ) clear_btn.click( _clear_chat, inputs=[inventory_state], outputs=[chat_html, display_history_state, llm_history_state, picker_sync, pending_emoji_state], show_progress="hidden", ).then( None, None, None, show_progress="hidden", js=""" () => { const target = document.querySelector('#picker-sync #picker-sync-data'); if (target && window.__renderEmojiPicker) { try { const raw = target.getAttribute('data-inv').replace(/"/g, '"'); const inv = JSON.parse(raw); window.__renderEmojiPicker(inv); } catch(e) { console.error(e); } } return []; } """ ) demo.load(None, None, None, js=PAGE_JS) demo.launch()