roemmele commited on
Commit
c275a0e
·
1 Parent(s): 79bbe79

First commit

Browse files
Files changed (3) hide show
  1. README.md +34 -7
  2. app.py +282 -4
  3. requirements.txt +2 -0
README.md CHANGED
@@ -1,14 +1,41 @@
1
  ---
2
  title: Creative Help
3
- emoji: 📈
4
- colorFrom: green
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.6.0
8
  app_file: app.py
9
  pinned: false
10
- license: cc-by-nc-sa-4.0
11
- short_description: A legacy app for LM-based writing assistance
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Creative Help
3
+ emoji: ✍️
4
+ colorFrom: red
5
+ colorTo: pink
6
  sdk: gradio
7
+ sdk_version: 5.0.0
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
 
11
  ---
12
 
13
+ # Creative Help
14
+
15
+ A word processor interface for eliciting text completions from the **creative-help** language model. Write freely, then type `\help\` to trigger generation automatically.
16
+
17
+ ## How to use
18
+
19
+ 1. **Write** your story in the text area.
20
+ 2. **Trigger** a completion by typing `\help\` at the end of your text — generation starts automatically.
21
+ 3. The model’s completion is appended to your text.
22
+
23
+ ## Setup (Space owner)
24
+
25
+ This Space calls your deployed **creative-help** Inference Endpoint. Configure these in **Settings → Variables and Secrets**:
26
+
27
+ | Name | Type | Description |
28
+ |------|------|-------------|
29
+ | `HF_ENDPOINT_URL` | Variable | Your Inference Endpoint URL (e.g. `https://xxx.us-east-1.aws.endpoints.huggingface.cloud`) |
30
+ | `HF_TOKEN` | Secret | Your Hugging Face token with access to the endpoint |
31
+
32
+ Alternatively, you can use the serverless Inference API by setting:
33
+
34
+ | Name | Type | Description |
35
+ |------|------|-------------|
36
+ | `HF_MODEL_ID` | Variable | Model ID (e.g. `username/creative-help`) |
37
+ | `HF_TOKEN` | Secret | Your Hugging Face token |
38
+
39
+ ## Model
40
+
41
+ This app uses the [creative-help](https://huggingface.co/YOUR_USERNAME/creative-help) model. Deploy it as an Inference Endpoint for best performance.
app.py CHANGED
@@ -1,7 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Creative Help - A word processor interface for eliciting text completions from the creative-help model.
3
+
4
+ Users write freely, then type \\help\\ to trigger generation automatically.
5
+ Deploy to Hugging Face Spaces: https://huggingface.co/spaces
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ import re
12
  import gradio as gr
13
+ import requests
14
+
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # --- Configuration ---
20
+ MAX_NEW_TOKENS = 50
21
+ TEMPERATURE = 1.0
22
+
23
+
24
+ def get_api_config():
25
+ """Get API endpoint and token from environment (Space secrets/variables)."""
26
+ # For Inference Endpoints: full URL like https://xxx.us-east-1.aws.endpoints.huggingface.cloud
27
+ # For Inference API: model ID like username/creative-help
28
+ endpoint_url = os.getenv("HF_ENDPOINT_URL", "").strip()
29
+ model_id = os.getenv("HF_MODEL_ID", "").strip()
30
+ token = os.getenv("HF_TOKEN", "").strip()
31
+
32
+ if endpoint_url:
33
+ # Inference Endpoint - use URL directly
34
+ return endpoint_url, token, True
35
+ if model_id:
36
+ # Inference API (serverless)
37
+ return f"https://api-inference.huggingface.co/models/{model_id}", token, False
38
+ return None, token, False
39
+
40
+
41
+ def call_creative_help(prompt: str, token: str, url: str) -> dict:
42
+ """Call the creative-help model API and return the result."""
43
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
44
+ payload = {
45
+ "inputs": prompt,
46
+ "parameters": {
47
+ "max_new_tokens": MAX_NEW_TOKENS,
48
+ "temperature": TEMPERATURE,
49
+ "do_sample": True,
50
+ },
51
+ }
52
+ logger.info("API request: url=%s prompt=%r parameters=%s",
53
+ url, prompt, payload["parameters"])
54
+ response = requests.post(url, headers=headers, json=payload, timeout=60)
55
+ response.raise_for_status()
56
+ result = response.json()
57
+ logger.info("API response: %s", result)
58
+ return result
59
+
60
+
61
+ def extract_prompt_and_trigger(text: str) -> tuple[str, bool]:
62
+ """
63
+ If text ends with \\help\\, return (text_without_trigger, True).
64
+ Otherwise return (text, False).
65
+ """
66
+ if not text or not text.strip():
67
+ return text, False
68
+ # Match \\help\\ at end (with optional trailing whitespace after it)
69
+ # Use greedy (.*) to preserve whitespace (e.g. paragraph breaks) before \help\
70
+ match = re.search(r"(.*)\\help\\\s*$", text, re.DOTALL)
71
+ if match:
72
+ # Don't rstrip; preserve trailing whitespace
73
+ return match.group(1), True
74
+ return text, False
75
+
76
+
77
+ def generate_completion(prompt: str) -> tuple[str | None, str]:
78
+ """
79
+ Call the API and append generated text to the prompt.
80
+ prompt is the text before \\help\\ (trigger already extracted).
81
+ Returns (updated_text, status_message). Returns (None, error_msg) on API/config errors.
82
+ """
83
+ url, token, _ = get_api_config()
84
+ if not url:
85
+ return None, "⚠️ Configure HF_ENDPOINT_URL or HF_MODEL_ID in Space Settings → Variables."
86
+ if not token:
87
+ return None, "⚠️ Configure HF_TOKEN in Space Settings → Secrets."
88
+
89
+ if not prompt.strip():
90
+ return None, "⚠️ Enter some text first to use as a prompt."
91
+
92
+ try:
93
+ result = call_creative_help(prompt, token, url)
94
+ except requests.exceptions.RequestException as e:
95
+ logger.exception("API request failed: %s", e)
96
+ return None, f"❌ API error: {e}"
97
+
98
+ # Parse response - handler returns [{"generated_text": "..."}]
99
+ if isinstance(result, list) and result and isinstance(result[0], dict):
100
+ generated = result[0].get("generated_text", "")
101
+ elif isinstance(result, dict):
102
+ if "error" in result:
103
+ return None, f"❌ {result['error']}"
104
+ generated = result.get("generated_text", "")
105
+ else:
106
+ return None, "❌ Unexpected API response format."
107
+
108
+ if not generated or not isinstance(generated, str):
109
+ return None, "❌ No generated text in response."
110
+
111
+ # Add single space only when prefix doesn't end with newline (avoids " ")
112
+ if prompt and prompt[-1].isspace():
113
+ new_text = prompt + generated.lstrip()
114
+ else:
115
+ new_text = prompt + generated
116
+
117
+ return new_text, f"✨ Generated: {generated[:80]}{'...' if len(generated) > 80 else ''}"
118
+
119
+
120
+ CURSOR_JS = """
121
+ (function() {
122
+ function findTextarea() {
123
+ const app = document.querySelector('gradio-app');
124
+ if (app && app.shadowRoot) {
125
+ return app.shadowRoot.querySelector('#story-input textarea');
126
+ }
127
+ return document.querySelector('#story-input textarea');
128
+ }
129
+ function moveCursorToEnd() {
130
+ const ta = findTextarea();
131
+ if (ta && !ta.disabled) {
132
+ ta.focus();
133
+ const len = ta.value.length;
134
+ ta.setSelectionRange(len, len);
135
+ }
136
+ }
137
+ function observeTextarea() {
138
+ const ta = findTextarea();
139
+ if (!ta) return false;
140
+ const observer = new MutationObserver(function(mutations) {
141
+ mutations.forEach(function(m) {
142
+ if (m.attributeName === 'disabled' && !ta.disabled) {
143
+ setTimeout(moveCursorToEnd, 0);
144
+ }
145
+ });
146
+ });
147
+ observer.observe(ta, { attributes: true, attributeFilter: ['disabled'] });
148
+ return true;
149
+ }
150
+ var attempts = 0;
151
+ var id = setInterval(function() {
152
+ if (observeTextarea() || ++attempts > 50) clearInterval(id);
153
+ }, 100);
154
+ })();
155
+ """
156
+
157
+
158
+ def get_model_repo_url() -> str:
159
+ return "https://huggingface.co/roemmele/creative-help"
160
+
161
+
162
+ def get_paper_url() -> str:
163
+ """Get the research paper URL for the footer link."""
164
+ url = os.getenv("PAPER_URL", "").strip()
165
+ if url:
166
+ return url
167
+ return "https://roemmele.github.io/publications/creative-help-demo.pdf"
168
+
169
+
170
+ APP_EXPLANATION = (
171
+ "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."
172
+ )
173
+
174
+
175
+ def create_ui():
176
+ """Build the Creative Help Gradio interface."""
177
+ with gr.Blocks(title="Creative Help", js=CURSOR_JS) as demo:
178
+ model_url = get_model_repo_url()
179
+ paper_url = get_paper_url()
180
+ header_icons = (
181
+ '<span class="header-icon header-icon-help">'
182
+ '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" '
183
+ 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
184
+ '<circle cx="12" cy="12" r="10"/>'
185
+ '<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>'
186
+ '<span class="help-tooltip">' +
187
+ APP_EXPLANATION.replace("<", "&lt;").replace(
188
+ ">", "&gt;") + '</span></span>'
189
+ )
190
+ gr.HTML(
191
+ '<div class="header-row">'
192
+ '<h1 class="creative-help-title">Creative Help</h1>'
193
+ '<div class="header-icons">' + header_icons + '</div>'
194
+ '</div>'
195
+ '<p class="instructions">Type <code>\\help\\</code> when you want to generate a suggestion.</p>'
196
+ )
197
+
198
+ with gr.Row():
199
+ textbox = gr.Textbox(
200
+ placeholder="Write your story here... Type \\help\\ when you want a suggestion.",
201
+ lines=12,
202
+ max_lines=20,
203
+ buttons=["copy"],
204
+ elem_id="story-input",
205
+ show_label=False,
206
+ )
207
+
208
+ def on_text_input(text: str):
209
+ prompt, had_trigger = extract_prompt_and_trigger(text or "")
210
+ if not had_trigger:
211
+ yield gr.skip()
212
+ return
213
+
214
+ # Preserve text (dimmed) while waiting, then replace with result
215
+ yield gr.update(value=text, interactive=False)
216
+ new_text, _ = generate_completion(prompt)
217
+ yield gr.update(value=new_text if new_text is not None else text, interactive=True)
218
+
219
+ # Use trigger_mode="always_last" so rapid typing only processes the final value.
220
+ textbox.input(
221
+ fn=on_text_input,
222
+ inputs=[textbox],
223
+ outputs=[textbox],
224
+ trigger_mode="always_last",
225
+ )
226
+
227
+ # Footer with model repo and paper links
228
+ gr.HTML(
229
+ f'<div class="footer">'
230
+ f'<a href="{model_url}" target="_blank" rel="noopener" class="footer-link">'
231
+ f'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" '
232
+ 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"/>'
233
+ 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>'
234
+ f' Model on Hugging Face</a>'
235
+ f'<a href="{paper_url}" target="_blank" rel="noopener" class="footer-link">'
236
+ f'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" '
237
+ 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"/>'
238
+ 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>'
239
+ f' Research paper</a></div>'
240
+ )
241
+
242
+ return demo
243
 
 
 
244
 
245
+ if __name__ == "__main__":
246
+ demo = create_ui()
247
+ demo.launch(
248
+ theme=gr.themes.Soft(primary_hue="red", secondary_hue="slate"),
249
+ css="""
250
+ .header-row { display: flex !important; align-items: center !important; gap: 0.75rem !important; margin-bottom: 0.25rem !important; }
251
+ .header-icons { display: flex !important; gap: 0.5rem !important; }
252
+ .header-icon { color: #718096 !important; cursor: pointer !important; transition: color 0.2s !important; }
253
+ .header-icon:hover { color: #c53030 !important; }
254
+ .header-icon svg { display: block !important; }
255
+ .header-icon-help { position: relative !important; }
256
+ .header-icon-help .help-tooltip {
257
+ visibility: hidden; opacity: 0; position: absolute; left: 50%; transform: translateX(-50%);
258
+ top: 100%; margin-top: 8px; padding: 10px 14px; background: #2d3748; color: #fff;
259
+ font-size: 0.85rem; line-height: 1.4; border-radius: 8px; width: 280px; text-align: left;
260
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 100; transition: opacity 0.2s, visibility 0.2s;
261
+ }
262
+ .header-icon-help:hover .help-tooltip { visibility: visible; opacity: 1; }
263
+ .creative-help-title { font-size: 2rem !important; font-weight: 700 !important; color: #c53030 !important; margin: 0 !important; }
264
+ .footer { margin-top: 1rem !important; padding-top: 0.75rem !important; border-top: 1px solid #e2e8f0 !important; }
265
+ .footer { display: flex !important; flex-wrap: wrap !important; gap: 1rem !important; }
266
+ .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; }
267
+ .footer-link:hover { color: #c53030 !important; }
268
+ .instructions { color: #4a5568; font-size: 0.95rem; margin-bottom: 1rem; }
269
+ /* Dim textbox while processing - remove border/outline from textarea and container */
270
+ #story-input textarea:disabled {
271
+ opacity: 0.25 !important;
272
+ background-color: #e2e8f0 !important;
273
+ filter: blur(0.5px);
274
+ border: none !important;
275
+ box-shadow: none !important;
276
+ outline: none !important;
277
+ }
278
+ #story-input:has(textarea:disabled),
279
+ #story-input:has(textarea:disabled) * {
280
+ border: none !important;
281
+ box-shadow: none !important;
282
+ outline: none !important;
283
+ }
284
+ """,
285
+ )
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=5.0
2
+ requests>=2.28