# ============================================================
# 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"?tool_call>", "", 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''
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("""