Spaces:
Running
Running
| 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) | |