HumanTouch / app.py
ciaochris's picture
Prevent HF iframe resizing from stretching HumanTouch
4f32ec2
Raw
History Blame Contribute Delete
23.8 kB
import os
import logging
import gradio as gr
from groq import Groq
from typing import Dict, List, Tuple
# --- Basic Configuration ---
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]
)
# --- The Core Application Logic ---
class HumanTouchApp:
def __init__(self):
api_key = os.environ.get("GROQ_API_KEY")
if not api_key:
raise EnvironmentError(
"FATAL: GROQ_API_KEY secret not found. "
"Please add it in the HuggingFace Space settings under 'Secrets'."
)
self.client = Groq(api_key=api_key)
logging.info("Groq client initialized successfully.")
self.model = "llama-3.3-70b-versatile"
self.system_prompt = self._load_system_prompt()
def _load_system_prompt(self) -> str:
return """
You are HumanTouch, a precise human editor and co-writer.
Preserve facts, names, numbers, claims, and intent. Do not invent details.
Remove machine-polished phrasing, generic filler, and forced metaphors.
Make the result sound specific, lived-in, and written by a thoughtful person.
You will receive two direction parameters: Style and Tone. Treat them as
constraints, not decorations.
For pasted text, rewrite the text. For a seed phrase, continue it into a
useful draft. Return only the finished text. No preamble, no explanation.
"""
def _build_style_tone_description(self, style: float, tone: float) -> str:
"""Translate raw slider values into descriptive language the model can act on."""
if style < 20:
style_desc = "plain and exact; every word earns its place, with no ornament"
elif style < 40:
style_desc = "clear and grounded, with just enough texture to feel human"
elif style < 60:
style_desc = "balanced; direct language with occasional concrete imagery"
elif style < 80:
style_desc = "more lyrical; vivid details are welcome, but clarity stays first"
else:
style_desc = "highly lyrical; use memorable rhythm and imagery without losing the point"
if tone < 20:
tone_desc = "reserved and formal; calm, measured, and professionally distant"
elif tone < 40:
tone_desc = "professional but approachable; warm without being casual"
elif tone < 60:
tone_desc = "conversational; natural, warm, and restrained"
elif tone < 80:
tone_desc = "personal and engaging; informal, but still polished"
else:
tone_desc = "expressive and heartfelt; emotionally alive without sounding inflated"
return (
f"Style direction: {style_desc}.\n"
f"Tone direction: {tone_desc}."
)
def _call_groq_api(self, user_content: str, style_level: float, tone_level: float) -> str:
try:
direction = self._build_style_tone_description(style_level, tone_level)
logging.info(f"Calling Groq API | Style: {style_level} | Tone: {tone_level}")
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": f"{direction}\n\n---\n\n{user_content}"}
]
# Temperature rises with style (more creative) and tone (more expressive),
# with style weighted more heavily. Range: ~0.5 (both 0) to ~1.25 (both 100).
temperature = 0.5 + (style_level / 200) + (tone_level / 400)
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=min(1.5, temperature),
max_tokens=1500,
)
return response.choices[0].message.content.strip()
except Exception as e:
logging.error(f"Error during Groq API call: {e}")
return (
"I could not reach the writing model just now. "
"Please try again in a moment."
)
def humanize_block_text(
self, text_to_humanize: str, style_level: float, tone_level: float
) -> str:
if not text_to_humanize.strip():
return "Paste a draft first, then I will rewrite it with the current style and tone."
return self._call_groq_api(text_to_humanize, style_level, tone_level)
def generate_co_creation(
self,
user_text: str,
chat_history: List[Dict[str, str]],
style_level: float,
tone_level: float,
) -> Tuple[List[Dict[str, str]], str]:
chat_history = chat_history or []
if not user_text.strip():
return chat_history, ""
ai_response = self._call_groq_api(user_text, style_level, tone_level)
return [
*chat_history,
{"role": "user", "content": user_text},
{"role": "assistant", "content": ai_response},
], ""
# --- The Gradio User Interface ---
def build_custom_css() -> str:
"""Return the visual theme CSS for the app."""
return """
:root {
color-scheme: light;
--ht-text: #17211d;
--ht-heading: #0d1714;
--ht-muted: #47564f;
--ht-surface: #ffffff;
--ht-surface-soft: #f8faf6;
--ht-panel: #fffffb;
--ht-border: rgba(23, 33, 29, 0.18);
--ht-border-strong: rgba(23, 33, 29, 0.30);
--ht-teal: #0f6f60;
--ht-coral: #c94e43;
--ht-amber: #b8791f;
--ht-navy: #223047;
--ht-focus: rgba(15, 111, 96, 0.26);
--ht-shadow: 0 18px 48px rgba(23, 33, 29, 0.10);
}
html,
body {
background: #f5f2e9;
}
body,
.gradio-container,
#main_container {
background:
linear-gradient(180deg, rgba(255, 255, 251, 0.97) 0%,
rgba(247, 250, 246, 0.95) 48%,
rgba(255, 247, 244, 0.94) 100%);
color: var(--ht-text) !important;
font-family: Aptos, "Segoe UI", Arial, sans-serif;
}
.gradio-container {
max-width: none !important;
--body-text-color: var(--ht-text);
--block-label-text-color: var(--ht-muted);
--input-background-fill: var(--ht-surface);
--input-border-color: var(--ht-border);
--background-fill-primary: var(--ht-panel);
--background-fill-secondary: var(--ht-surface-soft);
--border-color-primary: var(--ht-border);
--button-primary-background-fill: var(--ht-teal);
--button-primary-text-color: #ffffff;
}
.gradio-container * {
color: var(--ht-text);
letter-spacing: 0;
}
#main_container {
min-height: auto;
padding: 30px 32px 22px;
}
#header {
max-width: 1180px;
margin: 0 auto 20px;
text-align: left;
}
#header h1 {
color: var(--ht-heading) !important;
font-family: Georgia, "Times New Roman", serif;
font-size: 4.25rem;
font-weight: 650;
line-height: 0.96;
margin: 0 0 10px;
max-width: 100%;
}
#header .kicker {
margin: 0 0 10px;
color: var(--ht-teal) !important;
font-size: 0.78rem;
font-weight: 800;
text-transform: uppercase;
}
#header p {
max-width: 760px;
margin: 0;
color: var(--ht-muted) !important;
font-size: 1.05rem;
line-height: 1.6;
}
#error_banner {
max-width: 820px;
margin: 1rem auto;
padding: 1.1rem 1.25rem;
background: #fff4f2 !important;
border: 1px solid rgba(201, 78, 67, 0.38) !important;
border-radius: 8px !important;
}
#error_banner,
#error_banner * {
color: #7b2b25 !important;
}
.gradio-tabs,
.tabs {
max-width: 1180px;
margin: 0 auto;
padding: 14px;
background: var(--ht-panel) !important;
border: 1px solid var(--ht-border) !important;
border-radius: 8px !important;
box-shadow: var(--ht-shadow);
}
.tab-buttons {
gap: 8px;
border-bottom: 1px solid var(--ht-border);
margin-bottom: 14px;
}
.tab-buttons button,
button[role="tab"] {
min-height: 44px;
background: transparent !important;
border: 0 !important;
border-bottom: 3px solid transparent !important;
border-radius: 0 !important;
color: var(--ht-muted) !important;
font-size: 0.95rem;
font-weight: 750;
}
.tab-buttons button.selected,
button[role="tab"][aria-selected="true"],
button[role="tab"].selected {
color: var(--ht-heading) !important;
border-bottom-color: var(--ht-coral) !important;
}
.workspace-panel,
.control-panel,
.gradio-container .panel,
.gradio-container .block,
.gradio-container .form {
background: var(--ht-surface-soft) !important;
border-color: var(--ht-border) !important;
color: var(--ht-text) !important;
border-radius: 8px !important;
}
.humanizer-grid {
display: block !important;
width: 100% !important;
}
.canvas-layout {
display: block !important;
width: 100% !important;
}
.humanizer-grid > .form,
.humanizer-grid.form {
display: grid !important;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 16px;
align-items: stretch;
width: 100% !important;
}
.canvas-layout > .form,
.canvas-layout.form {
display: grid !important;
grid-template-columns: minmax(0, 3fr) minmax(280px, 1fr);
gap: 16px;
align-items: start;
width: 100% !important;
}
.humanizer-grid > .form > *,
.canvas-layout > .form > *,
.humanizer-grid.form > *,
.canvas-layout.form > * {
min-width: 0 !important;
width: 100% !important;
}
h2,
h3,
.gradio-container .prose h2,
.gradio-container .prose h3 {
color: var(--ht-heading) !important;
font-size: 1.02rem !important;
line-height: 1.35 !important;
}
.gradio-container p,
.gradio-container .prose,
.gradio-container .prose *,
.gradio-container .markdown,
.gradio-container .markdown * {
color: var(--ht-text) !important;
}
label,
.label-wrap,
.label-wrap span,
.block-label,
.caption,
.gradio-container .secondary-text {
color: var(--ht-muted) !important;
font-weight: 700 !important;
}
textarea,
input,
select,
[contenteditable="true"],
.gradio-container textarea,
.gradio-container input,
.gradio-container select {
background: var(--ht-surface) !important;
border-color: var(--ht-border-strong) !important;
color: var(--ht-text) !important;
-webkit-text-fill-color: var(--ht-text) !important;
caret-color: var(--ht-teal) !important;
font-size: 16px !important;
line-height: 1.55 !important;
}
textarea::placeholder,
input::placeholder {
color: #6a776f !important;
opacity: 1 !important;
-webkit-text-fill-color: #6a776f !important;
}
textarea:focus,
input:focus,
select:focus,
[contenteditable="true"]:focus {
border-color: var(--ht-teal) !important;
box-shadow: 0 0 0 4px var(--ht-focus) !important;
outline: none !important;
}
#humanizer_input, #humanizer_output,
#chat_input, #symbiotic_canvas {
background: var(--ht-surface) !important;
border: 1px solid var(--ht-border-strong) !important;
border-radius: 8px !important;
color: var(--ht-text) !important;
}
#humanizer_input textarea,
#humanizer_output textarea {
height: 260px !important;
min-height: 180px !important;
max-height: 320px !important;
}
#humanize_btn,
#compose_btn {
min-height: 48px;
background: var(--ht-teal) !important;
border: 1px solid #0b5d51 !important;
border-radius: 8px !important;
color: #ffffff !important;
font-size: 1rem !important;
font-weight: 800 !important;
transition: transform 160ms ease-out, box-shadow 160ms ease-out, background 160ms ease-out;
}
#humanize_btn *,
#compose_btn * {
color: #ffffff !important;
}
#humanize_btn:hover,
#compose_btn:hover {
background: #0b5d51 !important;
box-shadow: 0 12px 28px rgba(15, 111, 96, 0.22);
transform: translateY(-1px);
}
.seed-row button,
#example_btn {
min-height: 44px;
background: var(--ht-surface) !important;
border: 1px solid var(--ht-border-strong) !important;
border-radius: 8px !important;
color: var(--ht-text) !important;
font-weight: 750 !important;
}
.seed-row button:hover,
#example_btn:hover {
border-color: var(--ht-teal) !important;
box-shadow: 0 8px 20px rgba(34, 48, 71, 0.10);
}
#symbiotic_canvas {
height: 280px !important;
max-height: 320px !important;
overflow: auto;
}
#symbiotic_canvas .message,
#symbiotic_canvas .user,
#symbiotic_canvas .bot,
#symbiotic_canvas .assistant,
#symbiotic_canvas .message-wrap,
#symbiotic_canvas .bubble-wrap {
border-radius: 8px !important;
color: var(--ht-text) !important;
}
#symbiotic_canvas .message *,
#symbiotic_canvas .user *,
#symbiotic_canvas .bot *,
#symbiotic_canvas .assistant * {
color: var(--ht-text) !important;
-webkit-text-fill-color: var(--ht-text) !important;
}
#symbiotic_canvas .user {
background: #f8f2ea !important;
border: 1px solid rgba(184, 121, 31, 0.28) !important;
}
#symbiotic_canvas .bot,
#symbiotic_canvas .assistant {
background: #eef8f4 !important;
border: 1px solid rgba(15, 111, 96, 0.24) !important;
animation: bloom 220ms ease-out;
}
#symbiotic_canvas p {
color: var(--ht-text) !important;
font-size: 16px !important;
line-height: 1.55 !important;
}
#chat_input textarea {
min-height: 52px !important;
border-radius: 8px !important;
}
@keyframes bloom {
0% { opacity: 0; transform: translateY(4px); }
100% { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
@media (max-width: 900px) {
#header h1 {
font-size: 3rem;
}
}
@media (max-width: 768px) {
#main_container {
padding: 18px 12px 16px;
}
#header {
margin-bottom: 16px;
}
#header h1 {
font-size: 2.3rem;
}
#header p {
font-size: 1rem;
line-height: 1.5;
}
.gradio-tabs,
.tabs {
padding: 10px;
box-shadow: 0 12px 32px rgba(23, 33, 29, 0.08);
}
.humanizer-grid > .form,
.canvas-layout > .form,
.humanizer-grid.form,
.canvas-layout.form {
grid-template-columns: 1fr;
gap: 12px;
}
#humanizer_input textarea,
#humanizer_output textarea {
height: 140px !important;
min-height: 120px !important;
max-height: 180px !important;
}
#symbiotic_canvas {
height: 200px !important;
max-height: 220px !important;
}
#compose_btn {
width: 100%;
}
.tab-buttons button,
button[role="tab"] {
min-width: 0;
white-space: normal;
}
}
@media (max-width: 360px) {
#header h1 {
font-size: 2.05rem;
}
}
"""
def build_launch_kwargs() -> Dict[str, object]:
"""Return Gradio launch settings shared by local runs and Spaces."""
return {"debug": True, "css": build_custom_css()}
def create_interface():
# Fail loudly at startup so the error is visible, not buried in a response string.
try:
app = HumanTouchApp()
init_error = None
except EnvironmentError as e:
app = None
init_error = str(e)
logging.critical(init_error)
with gr.Blocks(title="HumanTouch") as interface:
with gr.Column(elem_id="main_container"):
with gr.Row(elem_id="header"):
gr.Markdown(
"<p class='kicker'>Vers3Dynamics writing studio</p>"
"<h1>HumanTouch</h1>"
"<p>Rewrite AI-polished text into prose with clearer intent, "
"cleaner rhythm, and a more human temperature.</p>"
)
# Surface a clear error banner if the app failed to initialize.
if init_error:
with gr.Row():
gr.Markdown(
f"## Configuration Error\n\n"
f"`{init_error}`\n\n"
f"Add your `GROQ_API_KEY` under **Settings → Secrets** in this HuggingFace Space, then restart.",
elem_id="error_banner"
)
else:
with gr.Tabs():
# ── Humanizer Tab ──────────────────────────────────────────
with gr.Tab("Humanizer", id="humanizer_tab"):
with gr.Column(variant="panel", elem_classes=["workspace-panel"]):
gr.Markdown("### Rewrite pasted text")
with gr.Row(elem_classes=["humanizer-grid"]):
input_text_humanizer = gr.Textbox(
label="Source text", lines=9,
placeholder="Paste a draft that sounds too polished, too generic, or too flat.",
elem_id="humanizer_input", scale=1
)
output_text_humanizer = gr.Textbox(
label="Human rewrite", lines=9,
interactive=False, elem_id="humanizer_output", scale=1
)
with gr.Row():
style_slider_h = gr.Slider(0, 100, 50, label="Style: clear to lyrical")
tone_slider_h = gr.Slider(0, 100, 50, label="Tone: reserved to expressive")
humanize_button = gr.Button("Humanize text", variant="primary", elem_id="humanize_btn")
example_button = gr.Button(
"Load concise technical example",
elem_id="example_btn",
variant="secondary",
)
# ── Co-Creative Canvas Tab ─────────────────────────────────
with gr.Tab("Co-Creative Canvas", id="canvas_tab"):
with gr.Row(equal_height=False, elem_classes=["canvas-layout"]):
with gr.Column(scale=3):
chatbot = gr.Chatbot(
[], label="Drafting canvas",
elem_id="symbiotic_canvas",
buttons=["copy", "copy_all"],
)
with gr.Row():
chat_input = gr.Textbox(
placeholder="Start with a sentence or outline.",
elem_id="chat_input",
show_label=False, container=False, scale=4
)
compose_btn = gr.Button(
"Compose", variant="primary",
scale=1, elem_id="compose_btn"
)
with gr.Column(scale=1, variant="panel", elem_classes=["control-panel"]):
gr.Markdown("### Resonance Controls")
style_slider_c = gr.Slider(0, 100, 50, label="Style: clear to lyrical")
tone_slider_c = gr.Slider(0, 100, 50, label="Tone: reserved to expressive")
gr.Markdown("### Seed phrases")
with gr.Row(elem_classes=["seed-row"]):
seed_city = gr.Button("City-at-night vignette", variant="secondary")
with gr.Row(elem_classes=["seed-row"]):
seed_email = gr.Button("Warm professional email", variant="secondary")
# ── Event Wiring ───────────────────────────────────────────────
humanize_button.click(
app.humanize_block_text,
[input_text_humanizer, style_slider_h, tone_slider_h],
[output_text_humanizer]
)
example_button.click(
lambda: "The system analyzed user behavior and determined that engagement increased by 14 percent after interface optimization.",
outputs=input_text_humanizer
)
compose_btn.click(
app.generate_co_creation,
[chat_input, chatbot, style_slider_c, tone_slider_c],
[chatbot, chat_input]
)
chat_input.submit(
app.generate_co_creation,
[chat_input, chatbot, style_slider_c, tone_slider_c],
[chatbot, chat_input]
)
seed_city.click(
lambda: "The city at night, after rain has made the pavement shine.",
outputs=chat_input
)
seed_email.click(
lambda: "Write a warm but professional opening for an email to my manager.",
outputs=chat_input
)
return interface
if __name__ == "__main__":
logging.info("Launching HumanTouch...")
try:
interface = create_interface()
interface.launch(**build_launch_kwargs())
except Exception as e:
logging.critical(f"Application launch failed: {e}", exc_info=True)