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( "
Vers3Dynamics writing studio
" "Rewrite AI-polished text into prose with clearer intent, " "cleaner rhythm, and a more human temperature.
" ) # 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)