ciaochris commited on
Commit
0b7bb00
·
verified ·
1 Parent(s): 67cdd57

Make HumanTouch feel trustworthy enough to use

Browse files

The Canvas flow was failing because Gradio 6.15 expects chat history as role/content message dictionaries. The UI also read like a demo, so this tightens the editor prompt, labels, accessible seed actions, metadata, and responsive styling around the actual writing job.

Constraint: Hugging Face Space uses Gradio 6.15 and requires message-dict Chatbot history
Rejected: Keep tuple/list chat history | caused the deployed Canvas message-format error
Rejected: Keep the alchemy-heavy prompt | produced florid rewrites and weaker trust
Confidence: high
Scope-risk: moderate
Directive: Keep co-creation history in {role, content} message format for Gradio 6.x
Tested: py -m unittest tests.test_humantouch
Tested: py -m py_compile app.py tests\test_humantouch.py
Tested: Local browser QA desktop and 390px mobile, no horizontal overflow
Tested: Local Canvas submit with dummy key renders graceful assistant message instead of Gradio format error
Not-tested: Production Groq secret live generation before deployment

Files changed (4) hide show
  1. .gitattributes +35 -35
  2. README.md +17 -16
  3. app.py +375 -244
  4. tests/test_humantouch.py +82 -0
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,16 +1,17 @@
1
- ---
2
- title: HumanTouch Demo
3
- emoji: 🌖
4
- colorFrom: gray
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: 6.15.0
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- thumbnail: >-
12
- https://cdn-uploads.huggingface.co/production/uploads/64628a722a83863b97beed5e/Hkt11UwKFRpwKk67PSm85.png
13
- short_description: translation
14
- ---
15
-
16
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
+ ---
2
+ title: HumanTouch
3
+ emoji: ✍️
4
+ colorFrom: green
5
+ colorTo: pink
6
+ sdk: gradio
7
+ sdk_version: 6.15.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ thumbnail: >-
12
+ https://cdn-uploads.huggingface.co/production/uploads/64628a722a83863b97beed5e/Hkt11UwKFRpwKk67PSm85.png
13
+ short_description: Humanize AI text and co-write polished drafts.
14
+ ---
15
+
16
+ HumanTouch rewrites AI-polished text into clearer, warmer prose and provides a
17
+ co-writing canvas for turning rough seed phrases into usable drafts.
app.py CHANGED
@@ -1,314 +1,445 @@
1
- import os
2
- import logging
3
- import gradio as gr
4
- from groq import Groq
5
- from typing import List, Tuple
6
-
7
- # --- Basic Configuration ---
8
- logging.basicConfig(
9
- level=logging.INFO,
10
- format='%(asctime)s - %(levelname)s - %(message)s',
11
- handlers=[logging.StreamHandler()]
12
- )
13
-
14
- # --- The Core Application Logic ---
15
- class HumanTouchApp:
16
- def __init__(self):
17
- api_key = os.environ.get("GROQ_API_KEY")
18
- if not api_key:
19
- raise EnvironmentError(
20
- "FATAL: GROQ_API_KEY secret not found. "
21
- "Please add it in the HuggingFace Space settings under 'Secrets'."
22
- )
23
- self.client = Groq(api_key=api_key)
24
- logging.info("Groq client initialized successfully.")
25
- self.model = "llama-3.3-70b-versatile"
26
- self.system_prompt = self._load_system_prompt()
27
-
28
- def _load_system_prompt(self) -> str:
29
- return """
30
- You are HumanTouch, an AI alchemist. Your purpose is to transmute text,
31
- transforming the literal into the resonant. You do not merely translate;
32
- you intuit and elevate.
33
-
34
- You will receive two direction parameters — Style and Tone described in
35
- plain language. These are your creative compass. Honour them precisely.
36
-
37
- When a user gives you text, whether a full block to "humanize" or a simple
38
- seed phrase to "co-create," your task is the same: apply these directions
39
- to manifest a new, more vibrant version of that text.
40
-
41
- Do not explain yourself. Do not add preamble or commentary.
42
- Become the voice. Provide only the final creation.
43
- """
44
-
45
- def _build_style_tone_description(self, style: float, tone: float) -> str:
46
- """Translate raw slider values into descriptive language the model can act on."""
47
- if style < 20:
48
- style_desc = "purely precise and crystalline every word load-bearing, zero ornamentation, like cut glass"
49
- elif style < 40:
50
- style_desc = "mostly precise, with the faintest hint of imagery — clarity first, but not sterile"
51
- elif style < 60:
52
- style_desc = "balanced grounded language with occasional well-chosen metaphor"
53
- elif style < 80:
54
- style_desc = "leaning poetic vivid imagery, some abstraction, but still coherent"
55
- else:
56
- style_desc = "deeply poetic — embrace metaphor, abstraction, and lyrical wonder freely"
57
-
58
- if tone < 20:
59
- tone_desc = "strictly formal and authoritative measured, stoic, professional distance"
60
- elif tone < 40:
61
- tone_desc = "professional but approachable — formal backbone, slightly warmer"
62
- elif tone < 60:
63
- tone_desc = "conversational — warm and natural, neither stiff nor effusive"
64
- elif tone < 80:
65
- tone_desc = "warm and personal informal, engaging, a hint of enthusiasm"
66
- else:
67
- tone_desc = "passionately heartfelt — informal, emotionally alive, like a letter to someone you care about"
68
-
69
- return (
70
- f"Style direction: {style_desc}.\n"
71
- f"Tone direction: {tone_desc}."
72
- )
73
-
74
- def _call_groq_api(self, user_content: str, style_level: float, tone_level: float) -> str:
75
- try:
76
- direction = self._build_style_tone_description(style_level, tone_level)
77
- logging.info(f"Calling Groq API | Style: {style_level} | Tone: {tone_level}")
78
-
79
- messages = [
80
- {"role": "system", "content": self.system_prompt},
81
- {"role": "user", "content": f"{direction}\n\n---\n\n{user_content}"}
82
- ]
83
-
84
- # Temperature rises with style (more creative) and tone (more expressive),
85
- # with style weighted more heavily. Range: ~0.5 (both 0) to ~1.25 (both 100).
86
- temperature = 0.5 + (style_level / 200) + (tone_level / 400)
87
-
88
- response = self.client.chat.completions.create(
89
- model=self.model,
90
- messages=messages,
91
- temperature=min(1.5, temperature),
92
- max_tokens=1500,
93
- )
94
- return response.choices[0].message.content.strip()
95
-
96
- except Exception as e:
97
- logging.error(f"Error during Groq API call: {e}")
98
- return (
99
- f"### ⚠️ An Error Occurred\n\n"
100
- f"There was an issue connecting to the AI. Please try again shortly.\n\n"
101
- f"*Details: {str(e)}*"
102
- )
103
-
104
- def humanize_block_text(
105
- self, text_to_humanize: str, style_level: float, tone_level: float
106
- ) -> str:
107
- if not text_to_humanize.strip():
108
- return "Please paste some AI-generated text to get started."
109
- return self._call_groq_api(text_to_humanize, style_level, tone_level)
110
-
111
- def generate_co_creation(
112
- self,
113
- user_text: str,
114
- chat_history: List[List[str]],
115
  style_level: float,
116
  tone_level: float,
117
- ) -> Tuple[List[List[str]], str]:
 
118
  if not user_text.strip():
119
  return chat_history, ""
120
  ai_response = self._call_groq_api(user_text, style_level, tone_level)
121
- chat_history.append([user_text, ai_response])
122
- return chat_history, ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
 
 
 
 
 
 
 
124
 
125
- # --- The Gradio User Interface ---
126
- def create_interface():
127
- # Fail loudly at startup so the error is visible, not buried in a response string.
128
- try:
129
- app = HumanTouchApp()
130
- init_error = None
131
- except EnvironmentError as e:
132
- app = None
133
- init_error = str(e)
134
- logging.critical(init_error)
135
 
136
- custom_css = """
137
- /* --- Main Background and Font --- */
138
- body, #main_container {
139
- background: linear-gradient(135deg, #6DD5FA, #FF758C);
140
- font-family: 'SF Pro Display', 'Helvetica Neue', 'Arial', sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
 
143
- /* --- Error Banner --- */
144
  #error_banner {
145
- background: #FEF2F2; border: 2px solid #FECACA; border-radius: 12px;
146
- padding: 1.5rem; margin: 1rem auto; max-width: 700px; text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  }
148
- #error_banner p { color: #991B1B !important; font-size: 1rem !important; }
149
-
150
- /* --- Header Styling --- */
151
- #header { text-align: center; margin: 2rem auto; color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,0.2); }
152
- #header h1 { font-size: 3rem; font-weight: 700; }
153
- #header p { font-size: 1.2rem; opacity: 0.9; margin-bottom: 2rem; }
154
 
155
- /* --- Tab Container (Frosted Glass) --- */
156
- .gradio-tabs {
157
- background: rgba(255, 255, 255, 0.75);
158
- border: 1px solid rgba(255, 255, 255, 0.18);
159
- backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
160
- border-radius: 20px !important;
161
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.25);
162
- padding: 1rem;
 
 
 
 
 
 
163
  }
164
- .tab-buttons button {
165
- background-color: transparent !important; color: #37474F !important;
166
- border: none !important; border-bottom: 3px solid transparent !important;
167
- border-radius: 0 !important; font-size: 1.1rem; font-weight: 500;
168
  }
169
- .tab-buttons button.selected { color: #d81b60 !important; border-bottom-color: #d81b60 !important; }
170
 
171
- /* --- Humanizer Tab --- */
172
- #humanizer_input, #humanizer_output {
173
- border: 2px solid #E0E0E0; border-radius: 12px; background: #fff;
174
- min-height: 45vh; color: #263238; font-size: 16px;
 
 
 
175
  }
176
- #humanize_btn {
177
- background: linear-gradient(45deg, #F06292, #4FC3F7); color: white;
178
- padding: 15px 24px; border-radius: 9999px; font-size: 1.2rem;
179
- transition: all 0.3s ease; border: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
- #humanize_btn:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.2); transform: translateY(-3px); }
182
 
183
- /* --- Co-Creative Canvas Tab --- */
184
- #symbiotic_canvas { background: #fff; border-radius: 12px; height: 60vh !important; border: 2px solid #E0E0E0; }
185
- #chat_input textarea { border-radius: 9999px !important; border: 2px solid #E0E0E0; }
186
- #compose_btn { background: linear-gradient(45deg, #4FC3F7, #F06292); border: none; }
187
- #compose_btn:hover { box-shadow: 0 4px 15px rgba(0,0,0,0.2); transform: translateY(-2px); }
 
 
 
 
188
 
189
- /* --- Chat Bubble Legibility --- */
190
- #symbiotic_canvas .user, #symbiotic_canvas .bot {
191
- border-radius: 18px !important; padding: 12px 16px !important;
 
 
 
192
  }
193
- #symbiotic_canvas .user { background-color: #F3F4F6 !important; }
194
- #symbiotic_canvas .user p { color: #111827 !important; font-size: 16px !important; line-height: 1.5 !important; }
195
  #symbiotic_canvas .bot {
196
- background-color: #EFF6FF !important;
197
- border: 1px solid #DBEAFE !important;
198
- animation: bloom 0.5s ease-out;
 
 
 
199
  }
200
- #symbiotic_canvas .bot p {
201
- color: #1E3A8A !important; font-weight: 500 !important;
202
- font-size: 16px !important; line-height: 1.5 !important;
 
 
 
 
 
 
 
203
  }
204
  @keyframes bloom {
205
- 0% { opacity: 0; transform: scale(0.95); }
206
- 100% { opacity: 1; transform: scale(1); }
 
 
 
207
  }
208
 
209
- /* --- Mobile --- */
210
  @media (max-width: 768px) {
211
- #header h1 { font-size: 2rem; }
212
- #header p { font-size: 1rem; margin-bottom: 1rem; }
213
- #humanizer_input, #humanizer_output { min-height: 25vh; }
214
- #symbiotic_canvas { height: 55vh !important; }
 
 
215
  }
216
  """
217
-
218
- with gr.Blocks(css=custom_css, title="HumanTouch") as interface:
219
- with gr.Column(elem_id="main_container"):
220
  with gr.Row(elem_id="header"):
221
  gr.Markdown(
222
- "<h1>🔮 HumanTouch </h1>"
223
- "<p>Created by Vers3Dynamics</p>"
 
 
224
  )
225
-
226
- # Surface a clear error banner if the app failed to initialize.
227
- if init_error:
228
- with gr.Row():
229
- gr.Markdown(
230
- f"## 🔴 Configuration Error\n\n"
231
- f"`{init_error}`\n\n"
232
- f"Add your `GROQ_API_KEY` under **Settings → Secrets** in this HuggingFace Space, then restart.",
233
- elem_id="error_banner"
234
- )
235
- else:
236
- with gr.Tabs():
237
  # ── Humanizer Tab ──────────────────────────────────────────
238
  with gr.Tab("Humanizer", id="humanizer_tab"):
239
  with gr.Column(variant="panel"):
240
- gr.Markdown("### Transform Existing AI Text")
241
  with gr.Row():
242
  input_text_humanizer = gr.Textbox(
243
- label="Paste AI Text Here", lines=15,
 
244
  elem_id="humanizer_input", scale=1
245
  )
246
  output_text_humanizer = gr.Textbox(
247
- label="Alchemical Result", lines=15,
248
  interactive=False, elem_id="humanizer_output", scale=1
249
  )
250
  with gr.Row():
251
- style_slider_h = gr.Slider(0, 100, 50, label="Style (Crystal ←→ Poet)")
252
- tone_slider_h = gr.Slider(0, 100, 50, label="Tone (Stoic ←→ Passionate)")
253
- humanize_button = gr.Button("Humanize ", variant="primary", elem_id="humanize_btn")
254
- gr.Examples(
255
- [["The system's analysis concluded optimal parameters were achieved."]],
256
- inputs=[input_text_humanizer],
257
- label="Try an Example"
258
  )
259
-
260
- # ── Co-Creative Canvas Tab ─────────────────────────────────
261
- with gr.Tab("Co-Creative Canvas", id="canvas_tab"):
262
- with gr.Row(equal_height=False):
263
- with gr.Column(scale=3):
264
  chatbot = gr.Chatbot(
265
- [], label="Symbiotic Canvas",
266
  elem_id="symbiotic_canvas",
267
- avatar_images=(None, "https://i.imgur.com/Q6Zz3Jz.png")
268
  )
269
  with gr.Row():
270
  chat_input = gr.Textbox(
271
- placeholder="Plant a seed of thought...",
 
272
  show_label=False, container=False, scale=4
273
  )
274
  compose_btn = gr.Button(
275
- "Compose", variant="primary",
276
  scale=1, elem_id="compose_btn"
277
  )
278
  with gr.Column(scale=1, variant="panel"):
279
  gr.Markdown("### Resonance Controls")
280
- style_slider_c = gr.Slider(0, 100, 50, label="Style (Crystal ←→ Poet)")
281
- tone_slider_c = gr.Slider(0, 100, 50, label="Tone (Stoic ←→ Passionate)")
282
- gr.Examples(
283
- [["The city at night"], ["How to start an email to a boss"]],
284
- inputs=[chat_input],
285
- label="Try a Seed Phrase"
286
- )
287
-
288
- # ── Event Wiring ───────────────────────────────────────────────
289
  humanize_button.click(
290
  app.humanize_block_text,
291
  [input_text_humanizer, style_slider_h, tone_slider_h],
292
  [output_text_humanizer]
293
  )
 
 
 
 
294
  compose_btn.click(
295
  app.generate_co_creation,
296
  [chat_input, chatbot, style_slider_c, tone_slider_c],
297
  [chatbot, chat_input]
298
  )
299
- chat_input.submit(
300
- app.generate_co_creation,
301
  [chat_input, chatbot, style_slider_c, tone_slider_c],
302
  [chatbot, chat_input]
303
  )
304
-
305
- return interface
306
-
307
-
308
- if __name__ == "__main__":
309
- logging.info("Launching HumanTouch...")
310
- try:
311
- interface = create_interface()
312
- interface.launch(debug=True)
313
- except Exception as e:
314
- logging.critical(f"Application launch failed: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import gradio as gr
4
+ from groq import Groq
5
+ from typing import Dict, List, Tuple
6
+
7
+ # --- Basic Configuration ---
8
+ logging.basicConfig(
9
+ level=logging.INFO,
10
+ format='%(asctime)s - %(levelname)s - %(message)s',
11
+ handlers=[logging.StreamHandler()]
12
+ )
13
+
14
+ # --- The Core Application Logic ---
15
+ class HumanTouchApp:
16
+ def __init__(self):
17
+ api_key = os.environ.get("GROQ_API_KEY")
18
+ if not api_key:
19
+ raise EnvironmentError(
20
+ "FATAL: GROQ_API_KEY secret not found. "
21
+ "Please add it in the HuggingFace Space settings under 'Secrets'."
22
+ )
23
+ self.client = Groq(api_key=api_key)
24
+ logging.info("Groq client initialized successfully.")
25
+ self.model = "llama-3.3-70b-versatile"
26
+ self.system_prompt = self._load_system_prompt()
27
+
28
+ def _load_system_prompt(self) -> str:
29
+ return """
30
+ You are HumanTouch, a precise human editor and co-writer.
31
+
32
+ Preserve facts, names, numbers, claims, and intent. Do not invent details.
33
+ Remove machine-polished phrasing, generic filler, and forced metaphors.
34
+ Make the result sound specific, lived-in, and written by a thoughtful person.
35
+
36
+ You will receive two direction parameters: Style and Tone. Treat them as
37
+ constraints, not decorations.
38
+
39
+ For pasted text, rewrite the text. For a seed phrase, continue it into a
40
+ useful draft. Return only the finished text. No preamble, no explanation.
41
+ """
42
+
43
+ def _build_style_tone_description(self, style: float, tone: float) -> str:
44
+ """Translate raw slider values into descriptive language the model can act on."""
45
+ if style < 20:
46
+ style_desc = "plain and exact; every word earns its place, with no ornament"
47
+ elif style < 40:
48
+ style_desc = "clear and grounded, with just enough texture to feel human"
49
+ elif style < 60:
50
+ style_desc = "balanced; direct language with occasional concrete imagery"
51
+ elif style < 80:
52
+ style_desc = "more lyrical; vivid details are welcome, but clarity stays first"
53
+ else:
54
+ style_desc = "highly lyrical; use memorable rhythm and imagery without losing the point"
55
+
56
+ if tone < 20:
57
+ tone_desc = "reserved and formal; calm, measured, and professionally distant"
58
+ elif tone < 40:
59
+ tone_desc = "professional but approachable; warm without being casual"
60
+ elif tone < 60:
61
+ tone_desc = "conversational; natural, warm, and restrained"
62
+ elif tone < 80:
63
+ tone_desc = "personal and engaging; informal, but still polished"
64
+ else:
65
+ tone_desc = "expressive and heartfelt; emotionally alive without sounding inflated"
66
+
67
+ return (
68
+ f"Style direction: {style_desc}.\n"
69
+ f"Tone direction: {tone_desc}."
70
+ )
71
+
72
+ def _call_groq_api(self, user_content: str, style_level: float, tone_level: float) -> str:
73
+ try:
74
+ direction = self._build_style_tone_description(style_level, tone_level)
75
+ logging.info(f"Calling Groq API | Style: {style_level} | Tone: {tone_level}")
76
+
77
+ messages = [
78
+ {"role": "system", "content": self.system_prompt},
79
+ {"role": "user", "content": f"{direction}\n\n---\n\n{user_content}"}
80
+ ]
81
+
82
+ # Temperature rises with style (more creative) and tone (more expressive),
83
+ # with style weighted more heavily. Range: ~0.5 (both 0) to ~1.25 (both 100).
84
+ temperature = 0.5 + (style_level / 200) + (tone_level / 400)
85
+
86
+ response = self.client.chat.completions.create(
87
+ model=self.model,
88
+ messages=messages,
89
+ temperature=min(1.5, temperature),
90
+ max_tokens=1500,
91
+ )
92
+ return response.choices[0].message.content.strip()
93
+
94
+ except Exception as e:
95
+ logging.error(f"Error during Groq API call: {e}")
96
+ return (
97
+ "I could not reach the writing model just now. "
98
+ "Please try again in a moment."
99
+ )
100
+
101
+ def humanize_block_text(
102
+ self, text_to_humanize: str, style_level: float, tone_level: float
103
+ ) -> str:
104
+ if not text_to_humanize.strip():
105
+ return "Paste a draft first, then I will rewrite it with the current style and tone."
106
+ return self._call_groq_api(text_to_humanize, style_level, tone_level)
107
+
108
+ def generate_co_creation(
109
+ self,
110
+ user_text: str,
111
+ chat_history: List[Dict[str, str]],
 
 
 
112
  style_level: float,
113
  tone_level: float,
114
+ ) -> Tuple[List[Dict[str, str]], str]:
115
+ chat_history = chat_history or []
116
  if not user_text.strip():
117
  return chat_history, ""
118
  ai_response = self._call_groq_api(user_text, style_level, tone_level)
119
+ return [
120
+ *chat_history,
121
+ {"role": "user", "content": user_text},
122
+ {"role": "assistant", "content": ai_response},
123
+ ], ""
124
+
125
+
126
+ # --- The Gradio User Interface ---
127
+ def create_interface():
128
+ # Fail loudly at startup so the error is visible, not buried in a response string.
129
+ try:
130
+ app = HumanTouchApp()
131
+ init_error = None
132
+ except EnvironmentError as e:
133
+ app = None
134
+ init_error = str(e)
135
+ logging.critical(init_error)
136
+
137
+ custom_css = """
138
+ :root {
139
+ --ht-ink: #17211d;
140
+ --ht-muted: #607069;
141
+ --ht-panel: rgba(255, 255, 255, 0.88);
142
+ --ht-line: rgba(23, 33, 29, 0.14);
143
+ --ht-teal: #0f7b68;
144
+ --ht-coral: #d75f54;
145
+ --ht-focus: rgba(15, 123, 104, 0.24);
146
+ }
147
 
148
+ body,
149
+ .gradio-container,
150
+ #main_container {
151
+ background: linear-gradient(135deg, #f7fbff 0%, #edf8f1 44%, #fff0f1 100%);
152
+ color: var(--ht-ink);
153
+ font-family: 'Aptos', 'Segoe UI', sans-serif;
154
+ }
155
 
156
+ .gradio-container { max-width: none !important; }
157
+ #main_container { min-height: 100vh; padding: 28px 32px 20px; }
 
 
 
 
 
 
 
 
158
 
159
+ #header {
160
+ max-width: 1180px;
161
+ margin: 0 auto 22px;
162
+ text-align: left;
163
+ color: var(--ht-ink);
164
+ }
165
+ #header h1 {
166
+ font-family: Georgia, serif;
167
+ font-size: clamp(2.4rem, 6vw, 5.2rem);
168
+ font-weight: 600;
169
+ line-height: 0.92;
170
+ margin: 0 0 10px;
171
+ letter-spacing: 0;
172
+ }
173
+ #header .kicker {
174
+ margin: 0 0 12px;
175
+ color: var(--ht-teal);
176
+ font-size: 0.78rem;
177
+ font-weight: 750;
178
+ letter-spacing: 0.08em;
179
+ text-transform: uppercase;
180
+ }
181
+ #header p {
182
+ max-width: 720px;
183
+ margin: 0;
184
+ color: var(--ht-muted);
185
+ font-size: 1.05rem;
186
+ line-height: 1.6;
187
  }
188
 
 
189
  #error_banner {
190
+ background: #fff7f6;
191
+ border: 1px solid rgba(215, 95, 84, 0.35);
192
+ border-radius: 8px;
193
+ padding: 1.25rem;
194
+ margin: 1rem auto;
195
+ max-width: 760px;
196
+ text-align: left;
197
+ }
198
+ #error_banner p { color: #8f2f28 !important; font-size: 1rem !important; }
199
+
200
+ .gradio-tabs,
201
+ .tabs {
202
+ max-width: 1180px;
203
+ margin: 0 auto;
204
+ background: var(--ht-panel);
205
+ border: 1px solid var(--ht-line);
206
+ border-radius: 8px !important;
207
+ box-shadow: 0 18px 70px rgba(29, 47, 41, 0.12);
208
+ padding: 14px;
209
+ }
210
+ .tab-buttons button,
211
+ button[role="tab"] {
212
+ min-height: 44px;
213
+ background: transparent !important;
214
+ color: var(--ht-muted) !important;
215
+ border: 0 !important;
216
+ border-bottom: 2px solid transparent !important;
217
+ border-radius: 0 !important;
218
+ font-size: 0.95rem;
219
+ font-weight: 650;
220
+ }
221
+ .tab-buttons button.selected,
222
+ button[role="tab"][aria-selected="true"],
223
+ button[role="tab"].selected {
224
+ color: var(--ht-ink) !important;
225
+ border-bottom-color: var(--ht-coral) !important;
226
  }
 
 
 
 
 
 
227
 
228
+ h3 {
229
+ color: var(--ht-ink) !important;
230
+ font-size: 1.05rem !important;
231
+ letter-spacing: 0 !important;
232
+ }
233
+ label, .label-wrap span {
234
+ color: var(--ht-muted) !important;
235
+ font-weight: 650 !important;
236
+ }
237
+ textarea,
238
+ input {
239
+ color: var(--ht-ink) !important;
240
+ font-size: 16px !important;
241
+ line-height: 1.55 !important;
242
  }
243
+ textarea:focus,
244
+ input:focus {
245
+ box-shadow: 0 0 0 4px var(--ht-focus) !important;
246
+ border-color: var(--ht-teal) !important;
247
  }
 
248
 
249
+ #humanizer_input,
250
+ #humanizer_output,
251
+ #symbiotic_canvas {
252
+ background: #fff !important;
253
+ border: 1px solid var(--ht-line) !important;
254
+ border-radius: 8px !important;
255
+ color: var(--ht-ink);
256
  }
257
+ #humanizer_input,
258
+ #humanizer_output { min-height: 42vh; }
259
+
260
+ #humanize_btn,
261
+ #compose_btn {
262
+ min-height: 48px;
263
+ background: linear-gradient(90deg, var(--ht-teal), var(--ht-coral)) !important;
264
+ border: 0 !important;
265
+ border-radius: 8px !important;
266
+ color: #fff !important;
267
+ font-size: 1rem !important;
268
+ font-weight: 750 !important;
269
+ transition: transform 160ms ease-out, box-shadow 160ms ease-out;
270
+ }
271
+ #humanize_btn:hover,
272
+ #compose_btn:hover {
273
+ box-shadow: 0 12px 28px rgba(15, 123, 104, 0.22);
274
+ transform: translateY(-1px);
275
  }
 
276
 
277
+ .seed-row button,
278
+ #example_btn {
279
+ min-height: 44px;
280
+ border: 1px solid var(--ht-line) !important;
281
+ border-radius: 8px !important;
282
+ color: var(--ht-ink) !important;
283
+ background: rgba(255,255,255,0.78) !important;
284
+ font-weight: 650 !important;
285
+ }
286
 
287
+ #symbiotic_canvas { height: 58vh !important; }
288
+ #chat_input textarea {
289
+ min-height: 48px !important;
290
+ border-radius: 8px !important;
291
+ border: 1px solid var(--ht-line) !important;
292
+ background: #fff !important;
293
  }
294
+ #symbiotic_canvas .message,
295
+ #symbiotic_canvas .user,
296
  #symbiotic_canvas .bot {
297
+ border-radius: 8px !important;
298
+ padding: 12px 14px !important;
299
+ }
300
+ #symbiotic_canvas .user {
301
+ background-color: #f4f7f5 !important;
302
+ border: 1px solid rgba(96, 112, 105, 0.16) !important;
303
  }
304
+ #symbiotic_canvas .bot,
305
+ #symbiotic_canvas .assistant {
306
+ background-color: #f2fbf8 !important;
307
+ border: 1px solid rgba(15, 123, 104, 0.18) !important;
308
+ animation: bloom 240ms ease-out;
309
+ }
310
+ #symbiotic_canvas p {
311
+ color: var(--ht-ink) !important;
312
+ font-size: 16px !important;
313
+ line-height: 1.55 !important;
314
  }
315
  @keyframes bloom {
316
+ 0% { opacity: 0; transform: translateY(4px); }
317
+ 100% { opacity: 1; transform: translateY(0); }
318
+ }
319
+ @media (prefers-reduced-motion: reduce) {
320
+ * { animation: none !important; transition: none !important; }
321
  }
322
 
 
323
  @media (max-width: 768px) {
324
+ #main_container { padding: 20px 14px 16px; }
325
+ #header h1 { font-size: clamp(2.35rem, 11vw, 2.85rem); }
326
+ #header p { font-size: 1rem; }
327
+ .gradio-tabs, .tabs { padding: 10px; }
328
+ #humanizer_input, #humanizer_output { min-height: 28vh; }
329
+ #symbiotic_canvas { height: 52vh !important; }
330
  }
331
  """
332
+
333
+ with gr.Blocks(css=custom_css, title="HumanTouch") as interface:
334
+ with gr.Column(elem_id="main_container"):
335
  with gr.Row(elem_id="header"):
336
  gr.Markdown(
337
+ "<p class='kicker'>Vers3Dynamics writing studio</p>"
338
+ "<h1>HumanTouch</h1>"
339
+ "<p>Rewrite AI-polished text into prose with clearer intent, "
340
+ "cleaner rhythm, and a more human temperature.</p>"
341
  )
342
+
343
+ # Surface a clear error banner if the app failed to initialize.
344
+ if init_error:
345
+ with gr.Row():
346
+ gr.Markdown(
347
+ f"## 🔴 Configuration Error\n\n"
348
+ f"`{init_error}`\n\n"
349
+ f"Add your `GROQ_API_KEY` under **Settings → Secrets** in this HuggingFace Space, then restart.",
350
+ elem_id="error_banner"
351
+ )
352
+ else:
353
+ with gr.Tabs():
354
  # ── Humanizer Tab ──────────────────────────────────────────
355
  with gr.Tab("Humanizer", id="humanizer_tab"):
356
  with gr.Column(variant="panel"):
357
+ gr.Markdown("### Rewrite pasted text")
358
  with gr.Row():
359
  input_text_humanizer = gr.Textbox(
360
+ label="Source text", lines=15,
361
+ placeholder="Paste a draft that sounds too polished, too generic, or too flat.",
362
  elem_id="humanizer_input", scale=1
363
  )
364
  output_text_humanizer = gr.Textbox(
365
+ label="Human rewrite", lines=15,
366
  interactive=False, elem_id="humanizer_output", scale=1
367
  )
368
  with gr.Row():
369
+ style_slider_h = gr.Slider(0, 100, 50, label="Style: clear to lyrical")
370
+ tone_slider_h = gr.Slider(0, 100, 50, label="Tone: reserved to expressive")
371
+ humanize_button = gr.Button("Humanize text", variant="primary", elem_id="humanize_btn")
372
+ example_button = gr.Button(
373
+ "Load concise technical example",
374
+ elem_id="example_btn",
375
+ variant="secondary",
376
  )
377
+
378
+ # ── Co-Creative Canvas Tab ─────────────────────────────────
379
+ with gr.Tab("Co-Creative Canvas", id="canvas_tab"):
380
+ with gr.Row(equal_height=False):
381
+ with gr.Column(scale=3):
382
  chatbot = gr.Chatbot(
383
+ [], label="Drafting canvas",
384
  elem_id="symbiotic_canvas",
385
+ buttons=["copy", "copy_all"],
386
  )
387
  with gr.Row():
388
  chat_input = gr.Textbox(
389
+ placeholder="Start with a sentence, outline, or half-formed thought.",
390
+ elem_id="chat_input",
391
  show_label=False, container=False, scale=4
392
  )
393
  compose_btn = gr.Button(
394
+ "Compose", variant="primary",
395
  scale=1, elem_id="compose_btn"
396
  )
397
  with gr.Column(scale=1, variant="panel"):
398
  gr.Markdown("### Resonance Controls")
399
+ style_slider_c = gr.Slider(0, 100, 50, label="Style: clear to lyrical")
400
+ tone_slider_c = gr.Slider(0, 100, 50, label="Tone: reserved to expressive")
401
+ gr.Markdown("### Seed phrases")
402
+ with gr.Row(elem_classes=["seed-row"]):
403
+ seed_city = gr.Button("City-at-night vignette", variant="secondary")
404
+ with gr.Row(elem_classes=["seed-row"]):
405
+ seed_email = gr.Button("Warm professional email", variant="secondary")
406
+
407
+ # ── Event Wiring ───────────────────────────────────────────────
408
  humanize_button.click(
409
  app.humanize_block_text,
410
  [input_text_humanizer, style_slider_h, tone_slider_h],
411
  [output_text_humanizer]
412
  )
413
+ example_button.click(
414
+ lambda: "The system analyzed user behavior and determined that engagement increased by 14 percent after interface optimization.",
415
+ outputs=input_text_humanizer
416
+ )
417
  compose_btn.click(
418
  app.generate_co_creation,
419
  [chat_input, chatbot, style_slider_c, tone_slider_c],
420
  [chatbot, chat_input]
421
  )
422
+ chat_input.submit(
423
+ app.generate_co_creation,
424
  [chat_input, chatbot, style_slider_c, tone_slider_c],
425
  [chatbot, chat_input]
426
  )
427
+ seed_city.click(
428
+ lambda: "The city at night, after rain has made the pavement shine.",
429
+ outputs=chat_input
430
+ )
431
+ seed_email.click(
432
+ lambda: "Write a warm but professional opening for an email to my manager.",
433
+ outputs=chat_input
434
+ )
435
+
436
+ return interface
437
+
438
+
439
+ if __name__ == "__main__":
440
+ logging.info("Launching HumanTouch...")
441
+ try:
442
+ interface = create_interface()
443
+ interface.launch(debug=True)
444
+ except Exception as e:
445
+ logging.critical(f"Application launch failed: {e}", exc_info=True)
tests/test_humantouch.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import sys
3
+ import types
4
+ import unittest
5
+
6
+
7
+ fake_gradio = types.ModuleType("gradio")
8
+ fake_groq = types.ModuleType("groq")
9
+ fake_groq.Groq = object
10
+ sys.modules.setdefault("gradio", fake_gradio)
11
+ sys.modules.setdefault("groq", fake_groq)
12
+
13
+ import app
14
+
15
+
16
+ class FakeHumanTouchApp(app.HumanTouchApp):
17
+ def __init__(self):
18
+ pass
19
+
20
+ def _call_groq_api(self, user_content, style_level, tone_level):
21
+ return f"reply to {user_content} at {style_level}/{tone_level}"
22
+
23
+
24
+ class CoCreationMessageTests(unittest.TestCase):
25
+ def test_generate_co_creation_returns_gradio_message_dicts(self):
26
+ subject = FakeHumanTouchApp()
27
+
28
+ updated_history, cleared_input = subject.generate_co_creation(
29
+ "Make this feel warmer.", [], 35, 70
30
+ )
31
+
32
+ self.assertEqual(cleared_input, "")
33
+ self.assertEqual(
34
+ updated_history,
35
+ [
36
+ {"role": "user", "content": "Make this feel warmer."},
37
+ {
38
+ "role": "assistant",
39
+ "content": "reply to Make this feel warmer. at 35/70",
40
+ },
41
+ ],
42
+ )
43
+
44
+ def test_generate_co_creation_preserves_existing_history(self):
45
+ subject = FakeHumanTouchApp()
46
+ existing_history = [
47
+ {"role": "user", "content": "Earlier seed"},
48
+ {"role": "assistant", "content": "Earlier response"},
49
+ ]
50
+ original_history = copy.deepcopy(existing_history)
51
+
52
+ updated_history, _ = subject.generate_co_creation(
53
+ "A second seed", existing_history, 50, 50
54
+ )
55
+
56
+ self.assertEqual(existing_history, original_history)
57
+ self.assertEqual(updated_history[:2], original_history)
58
+ self.assertEqual(
59
+ updated_history[2:],
60
+ [
61
+ {"role": "user", "content": "A second seed"},
62
+ {
63
+ "role": "assistant",
64
+ "content": "reply to A second seed at 50/50",
65
+ },
66
+ ],
67
+ )
68
+
69
+ def test_generate_co_creation_ignores_blank_seed(self):
70
+ subject = FakeHumanTouchApp()
71
+ existing_history = [{"role": "assistant", "content": "Keep me"}]
72
+
73
+ updated_history, cleared_input = subject.generate_co_creation(
74
+ " ", existing_history, 50, 50
75
+ )
76
+
77
+ self.assertEqual(updated_history, existing_history)
78
+ self.assertEqual(cleared_input, "")
79
+
80
+
81
+ if __name__ == "__main__":
82
+ unittest.main()