Spaces:
Sleeping
Sleeping
File size: 11,414 Bytes
c275a0e 79bbe79 c275a0e 3c6f0c1 c275a0e 58bad40 c275a0e 58bad40 c275a0e 3c6f0c1 c275a0e 58bad40 c275a0e 58bad40 c275a0e 58bad40 c275a0e 79bbe79 c275a0e 58bad40 | 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 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | """
Creative Help - A word processor interface for eliciting text completions from the creative-help model.
Users write freely, then type \\help\\ to trigger generation automatically.
Deploy to Hugging Face Spaces: https://huggingface.co/spaces
"""
from __future__ import annotations
import logging
import os
import re
import gradio as gr
import requests
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Configuration ---
MAX_NEW_TOKENS = 50
TEMPERATURE = 1.0
def get_api_config():
"""Get API endpoint and token from environment (Space secrets/variables)."""
endpoint_url = os.getenv("HF_ENDPOINT_URL", "").strip()
token = os.getenv("HF_TOKEN", "").strip()
return endpoint_url, token
def call_creative_help(prompt: str, token: str, url: str) -> dict:
"""Call the creative-help model API and return the result."""
headers = {"Authorization": f"Bearer {token}"} if token else {}
payload = {
"inputs": prompt,
"parameters": {
"max_new_tokens": MAX_NEW_TOKENS,
"temperature": TEMPERATURE,
"do_sample": True,
},
}
logger.info("API request: url=%s prompt=%r parameters=%s",
url, prompt, payload["parameters"])
response = requests.post(url, headers=headers, json=payload, timeout=60)
response.raise_for_status()
result = response.json()
logger.info("API response: %s", result)
return result
def extract_prompt_and_trigger(text: str) -> tuple[str, bool]:
"""
If text ends with \\help\\ or /help/, return (text_without_trigger, True).
Otherwise return (text, False).
"""
if not text or not text.strip():
return text, False
# Match \help\ or /help/ at end (with optional trailing whitespace)
# Use greedy (.*) to preserve whitespace (e.g. paragraph breaks) before trigger
for pattern in (r"(.*)\\help\\\s*$", r"(.*)/help/\s*$"):
match = re.search(pattern, text, re.DOTALL)
if match:
return match.group(1), True
return text, False
def generate_completion(prompt: str) -> tuple[str | None, str]:
"""
Call the API and append generated text to the prompt.
prompt is the text before \\help\\ (trigger already extracted).
Returns (updated_text, status_message). Returns (None, error_msg) on API/config errors.
"""
url, token = get_api_config()
if not url:
return None, "⚠️ Configure HF_ENDPOINT_URL or HF_MODEL_ID in Space Settings → Variables."
if not token:
return None, "⚠️ Configure HF_TOKEN in Space Settings → Secrets."
if not prompt.strip():
return None, "⚠️ Enter some text first to use as a prompt."
try:
result = call_creative_help(prompt, token, url)
except requests.exceptions.RequestException as e:
logger.exception("API request failed: %s", e)
return None, f"❌ API error: {e}"
# Parse response - handler returns [{"generated_text": "..."}]
if isinstance(result, list) and result and isinstance(result[0], dict):
generated = result[0].get("generated_text", "")
elif isinstance(result, dict):
if "error" in result:
return None, f"❌ {result['error']}"
generated = result.get("generated_text", "")
else:
return None, "❌ Unexpected API response format."
if not generated or not isinstance(generated, str):
return None, "❌ No generated text in response."
# Add single space only when prefix doesn't end with newline (avoids " ")
if prompt and prompt[-1].isspace():
new_text = prompt + generated.lstrip()
else:
new_text = prompt + generated
return new_text, f"✨ Generated: {generated[:80]}{'...' if len(generated) > 80 else ''}"
CURSOR_JS = """
(function() {
function findTextarea() {
const app = document.querySelector('gradio-app');
if (app && app.shadowRoot) {
return app.shadowRoot.querySelector('#story-input textarea');
}
return document.querySelector('#story-input textarea');
}
function moveCursorToEnd() {
const ta = findTextarea();
if (ta && !ta.disabled) {
ta.focus();
const len = ta.value.length;
ta.setSelectionRange(len, len);
}
}
function observeTextarea() {
const ta = findTextarea();
if (!ta) return false;
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.attributeName === 'disabled' && !ta.disabled) {
setTimeout(moveCursorToEnd, 0);
}
});
});
observer.observe(ta, { attributes: true, attributeFilter: ['disabled'] });
return true;
}
var attempts = 0;
var id = setInterval(function() {
if (observeTextarea() || ++attempts > 50) clearInterval(id);
}, 100);
})();
"""
def get_model_repo_url() -> str:
return "https://huggingface.co/roemmele/creative-help"
def get_paper_url() -> str:
"""Get the research paper URL for the footer link."""
url = os.getenv("PAPER_URL", "").strip()
if url:
return url
return "https://roemmele.github.io/publications/creative-help-demo.pdf"
APP_EXPLANATION = (
"Creative Help is a legacy app for AI-based writing assistance. It was developed in 2016 as one of the first demonstrations of the use of a language model for helping people write stories."
)
CUSTOM_CSS = """
.header-row { display: flex !important; align-items: center !important; gap: 0.75rem !important; margin-bottom: 0.25rem !important; }
.header-icons { display: flex !important; gap: 0.5rem !important; }
.header-icon { color: #718096 !important; cursor: pointer !important; transition: color 0.2s !important; }
.header-icon:hover { color: #c53030 !important; }
.header-icon svg { display: block !important; }
.header-icon-help { position: relative !important; }
.header-icon-help .help-tooltip {
visibility: hidden; opacity: 0; position: absolute; left: 50%; transform: translateX(-50%);
top: 100%; margin-top: 8px; padding: 10px 14px; background: #2d3748; color: #fff;
font-size: 0.85rem; line-height: 1.4; border-radius: 8px; width: 280px; text-align: left;
box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 100; transition: opacity 0.2s, visibility 0.2s;
}
.header-icon-help:hover .help-tooltip { visibility: visible; opacity: 1; }
.creative-help-title { font-size: 2rem !important; font-weight: 700 !important; color: #c53030 !important; margin: 0 !important; }
.footer { margin-top: 1rem !important; padding-top: 0.75rem !important; border-top: 1px solid #e2e8f0 !important; }
.footer { display: flex !important; flex-wrap: wrap !important; gap: 1rem !important; }
.footer-link { color: #718096 !important; font-size: 0.875rem !important; text-decoration: none !important; display: inline-flex !important; align-items: center !important; gap: 0.35rem !important; }
.footer-link:hover { color: #c53030 !important; }
.instructions { color: #4a5568; font-size: 0.95rem; margin-bottom: 1rem; }
/* Dim textbox while processing - remove border/outline from textarea and container */
#story-input textarea:disabled {
opacity: 0.25 !important;
background-color: #e2e8f0 !important;
filter: blur(0.5px);
border: none !important;
box-shadow: none !important;
outline: none !important;
}
#story-input:has(textarea:disabled),
#story-input:has(textarea:disabled) * {
border: none !important;
box-shadow: none !important;
outline: none !important;
}
"""
def create_ui():
"""Build the Creative Help Gradio interface."""
with gr.Blocks(
title="Creative Help",
js=CURSOR_JS,
theme=gr.themes.Soft(primary_hue="red", secondary_hue="slate"),
css=CUSTOM_CSS,
) as demo:
model_url = get_model_repo_url()
paper_url = get_paper_url()
header_icons = (
'<span class="header-icon header-icon-help">'
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" '
'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
'<circle cx="12" cy="12" r="10"/>'
'<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>'
'<span class="help-tooltip">' +
APP_EXPLANATION.replace("<", "<").replace(
">", ">") + '</span></span>'
)
gr.HTML(
'<div class="header-row">'
'<h1 class="creative-help-title">Creative Help</h1>'
'<div class="header-icons">' + header_icons + '</div>'
'</div>'
'<p class="instructions">Type <code>\\help\\</code> when you want to generate a suggestion.</p>'
)
with gr.Row():
textbox = gr.Textbox(
placeholder="Write your story here... Type \\help\\ when you want a suggestion.",
lines=12,
max_lines=20,
elem_id="story-input",
show_label=False,
)
def on_text_input(text: str):
prompt, had_trigger = extract_prompt_and_trigger(text or "")
if not had_trigger:
# Use gr.skip() if available (Gradio 5+), else no-op update for Gradio 4
if hasattr(gr, "skip"):
yield gr.skip()
else:
yield gr.update(value=text)
return
# Preserve text (dimmed) while waiting, then replace with result
yield gr.update(value=text, interactive=False)
new_text, _ = generate_completion(prompt)
yield gr.update(value=new_text if new_text is not None else text, interactive=True)
# Use trigger_mode="always_last" so rapid typing only processes the final value.
textbox.input(
fn=on_text_input,
inputs=[textbox],
outputs=[textbox],
trigger_mode="always_last",
)
# Footer with model repo and paper links
gr.HTML(
f'<div class="footer">'
f'<a href="{model_url}" target="_blank" rel="noopener" class="footer-link">'
f'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" '
f'fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>'
f'<polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>'
f' Model on Hugging Face</a>'
f'<a href="{paper_url}" target="_blank" rel="noopener" class="footer-link">'
f'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" '
f'fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>'
f'<polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>'
f' Research paper</a></div>'
)
return demo
if __name__ == "__main__":
demo = create_ui()
demo.launch()
|