tts / app.py
callmekvj's picture
updated app.py
2e57212 verified
Raw
History Blame Contribute Delete
16.4 kB
# ── Patch gradio_client bug (gradio 4.x) ──────────────────────────────────────
try:
import gradio_client.utils as _gcu
_orig = _gcu.get_type
def _safe(s):
if not isinstance(s, dict): return "any"
return _orig(s)
_gcu.get_type = _safe
except Exception:
pass
import base64, httpx, uuid, os
from pathlib import Path
from transformers import pipeline
import gradio as gr
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
Path("audio_output").mkdir(exist_ok=True)
print("Loading emotion model...")
emotion_classifier = pipeline(
"text-classification",
model="j-hartmann/emotion-english-distilroberta-base",
top_k=1
)
print("Model loaded.")
# Load API credentials from environment variables
INWORLD_API_KEY = os.getenv("INWORLD_API_KEY")
INWORLD_VOICE_ID = os.getenv("INWORLD_VOICE_ID", "Clive")
if not INWORLD_API_KEY:
raise ValueError(
"[ERROR] INWORLD_API_KEY not found in environment variables. "
"Please create a .env file with INWORLD_API_KEY=your_key_here"
)
EMOTION_CONFIG = {
"joy": {"label": "Joyful", "base_rate": 1.25, "base_temp": 1.3, "rate_boost": 0.25, "temp_boost": 0.4},
"surprise": {"label": "Surprised", "base_rate": 1.2, "base_temp": 1.4, "rate_boost": 0.2, "temp_boost": 0.35},
"anger": {"label": "Angry", "base_rate": 0.85, "base_temp": 0.5, "rate_boost": -0.1, "temp_boost": -0.15},
"disgust": {"label": "Disgusted", "base_rate": 0.8, "base_temp": 0.55, "rate_boost": -0.1, "temp_boost": -0.1},
"fear": {"label": "Fearful", "base_rate": 1.1, "base_temp": 1.2, "rate_boost": 0.15, "temp_boost": 0.25},
"sadness": {"label": "Sad", "base_rate": 0.7, "base_temp": 0.45, "rate_boost": -0.1, "temp_boost": -0.1},
"neutral": {"label": "Neutral", "base_rate": 1.0, "base_temp": 1.0, "rate_boost": 0.0, "temp_boost": 0.0},
}
session_emotions = {}
session_history = []
def synthesize(text):
global session_emotions, session_history
if not text or not text.strip():
return None, "[WARNING] Please enter some text.", "", "", ""
text = text.strip()
results = emotion_classifier(text)
top = results[0][0]
emotion = top["label"].lower()
score = float(top["score"])
intensity = round(score * 100, 1)
cfg = EMOTION_CONFIG.get(emotion, EMOTION_CONFIG["neutral"])
rate = round(max(0.5, min(2.0, cfg["base_rate"] + cfg["rate_boost"] * score)), 3)
temp = round(max(0.1, min(2.0, cfg["base_temp"] + cfg["temp_boost"] * score)), 3)
try:
resp = httpx.post(
"https://api.inworld.ai/tts/v1/voice",
headers={"Authorization": f"Basic {INWORLD_API_KEY}", "Content-Type": "application/json"},
json={"text": text, "voiceId": INWORLD_VOICE_ID, "modelId": "inworld-tts-1.5-max",
"speakingRate": rate, "temperature": temp},
timeout=30,
)
if resp.status_code != 200:
return None, f"[ERROR] Inworld error {resp.status_code}: {resp.text[:200]}", "", "", ""
audio_b64 = resp.json().get("audioContent", "")
if not audio_b64:
return None, "[ERROR] No audio returned.", "", "", ""
filepath = f"audio_output/{uuid.uuid4().hex}.mp3"
Path(filepath).write_bytes(base64.b64decode(audio_b64))
except Exception as e:
return None, f"[ERROR] {e}", "", "", ""
session_emotions[emotion] = session_emotions.get(emotion, 0) + 1
session_history.insert(0, {"text": text, "label": cfg["label"],
"intensity": intensity, "rate": rate})
if len(session_history) > 20:
session_history.pop()
total = len(session_history)
dom = max(session_emotions, key=session_emotions.get)
dom_cfg = EMOTION_CONFIG[dom]
avg_c = round(sum(h["intensity"] for h in session_history) / total, 1)
emotion_text = (
f"[{cfg['label'].upper()}]\n\n"
f" Confidence {intensity}%\n"
f" Speaking Rate {rate}x\n"
f" Temperature {temp}"
)
stats_text = (
f" Synthesized {total} clips\n"
f" Top Emotion {dom_cfg['label']}\n"
f" Avg Conf. {avg_c}%"
)
max_c = max(session_emotions.values())
dist_lines = []
for e, cnt in sorted(session_emotions.items(), key=lambda x: -x[1]):
c = EMOTION_CONFIG[e]
filled = round((cnt / max_c) * 20)
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
dist_lines.append(f" {c['label']:<12} {bar} {cnt}")
dist_text = "\n".join(dist_lines)
hist_lines = [
f" {h['label']:<12} {h['intensity']:>5}% {h['text'][:50]}"
for h in session_history[:10]
]
hist_text = "\n".join(hist_lines)
return filepath, emotion_text, stats_text, dist_text, hist_text
SAMPLES = [
"I just got the promotion I've been working towards for years!",
"This is completely unacceptable behaviour from your team.",
"I really miss those days. It all feels so far away now.",
"Wait β€” that actually cannot be real, right?!",
"The meeting has been rescheduled to 3pm tomorrow.",
"I heard something strange coming from downstairs at 2am.",
]
CSS = """
/* ── FULL DARK THEME ── */
*, *::before, *::after { box-sizing: border-box; }
body {
background: #0d1117 !important;
color: #e6edf3 !important;
}
.gradio-container {
background: #0d1117 !important;
max-width: 1000px !important;
margin: 0 auto !important;
font-family: 'Inter', system-ui, sans-serif !important;
color: #e6edf3 !important;
}
/* All blocks */
.block, .form, .box {
background: #161b22 !important;
border: 1px solid #30363d !important;
border-radius: 10px !important;
color: #e6edf3 !important;
}
/* Labels */
label > span, .label-wrap span {
color: #8b949e !important;
font-size: 11px !important;
font-weight: 600 !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
}
/* Textareas and inputs */
textarea, input[type=text] {
background: #0d1117 !important;
border: 1px solid #30363d !important;
border-radius: 8px !important;
color: #e6edf3 !important;
font-size: 14px !important;
caret-color: #58a6ff !important;
}
textarea:focus, input:focus {
border-color: #58a6ff !important;
box-shadow: 0 0 0 3px rgba(88,166,255,0.1) !important;
outline: none !important;
}
textarea::placeholder, input::placeholder {
color: #484f58 !important;
}
/* Output textboxes */
.output-box textarea {
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace !important;
font-size: 13px !important;
line-height: 1.75 !important;
color: #cdd9e5 !important;
background: #0d1117 !important;
}
/* Button */
button.primary {
background: #238636 !important;
border: 1px solid #2ea043 !important;
border-radius: 8px !important;
color: #ffffff !important;
font-weight: 600 !important;
font-size: 14px !important;
padding: 10px 24px !important;
transition: background 0.15s !important;
}
button.primary:hover {
background: #2ea043 !important;
}
button.secondary {
background: #21262d !important;
border: 1px solid #30363d !important;
color: #cdd9e5 !important;
border-radius: 8px !important;
}
/* Markdown text */
.markdown, .prose, .md {
color: #cdd9e5 !important;
}
.markdown h1, .prose h1 { color: #e6edf3 !important; font-size: 1.8rem !important; font-weight: 700 !important; }
.markdown h2, .prose h2 { color: #e6edf3 !important; font-size: 1.3rem !important; font-weight: 600 !important; margin-top: 20px !important; }
.markdown h3, .prose h3 { color: #cdd9e5 !important; font-size: 1.05rem !important; font-weight: 600 !important; margin-top: 16px !important; }
.markdown p, .prose p { color: #8b949e !important; font-size: 14px !important; line-height: 1.65 !important; }
.markdown li, .prose li { color: #8b949e !important; font-size: 14px !important; line-height: 1.7 !important; }
.markdown strong, .prose strong { color: #cdd9e5 !important; }
.markdown a, .prose a { color: #58a6ff !important; text-decoration: none !important; }
.markdown a:hover, .prose a:hover { text-decoration: underline !important; }
.markdown hr, .prose hr { border-color: #30363d !important; margin: 18px 0 !important; }
.markdown blockquote, .prose blockquote {
border-left: 3px solid #388bfd !important;
margin: 12px 0 !important;
padding: 8px 16px !important;
background: #1c2128 !important;
border-radius: 0 6px 6px 0 !important;
color: #8b949e !important;
}
/* Inline code */
.markdown code, .prose code, code {
background: #1c2128 !important;
color: #79c0ff !important;
padding: 2px 6px !important;
border-radius: 4px !important;
font-size: 12px !important;
font-family: 'JetBrains Mono', monospace !important;
border: 1px solid #30363d !important;
}
/* Tables */
table { width: 100% !important; border-collapse: collapse !important; background: transparent !important; }
thead tr { background: #1c2128 !important; }
th {
background: #1c2128 !important;
color: #8b949e !important;
font-size: 12px !important;
font-weight: 600 !important;
padding: 10px 14px !important;
text-align: left !important;
border-bottom: 1px solid #30363d !important;
}
td {
background: transparent !important;
color: #cdd9e5 !important;
font-size: 13px !important;
padding: 9px 14px !important;
border-bottom: 1px solid #21262d !important;
}
tr:last-child td { border-bottom: none !important; }
tr:hover td { background: #1c2128 !important; }
/* Accordion / details */
details {
background: #161b22 !important;
border: 1px solid #30363d !important;
border-radius: 10px !important;
overflow: hidden !important;
}
details > summary {
background: #161b22 !important;
color: #cdd9e5 !important;
font-weight: 600 !important;
font-size: 14px !important;
padding: 14px 18px !important;
cursor: pointer !important;
list-style: none !important;
}
details > summary:hover { background: #1c2128 !important; }
details[open] > summary { border-bottom: 1px solid #30363d !important; }
details .markdown p, details .prose p { color: #8b949e !important; }
details .markdown li, details .prose li { color: #8b949e !important; }
/* Examples */
.examples {
background: #161b22 !important;
border: 1px solid #30363d !important;
border-radius: 8px !important;
}
.examples table td {
background: transparent !important;
color: #8b949e !important;
font-size: 12px !important;
border-bottom: 1px solid #21262d !important;
}
.examples table th {
background: #1c2128 !important;
color: #8b949e !important;
}
.examples table tr:hover td { background: #1c2128 !important; }
/* Audio */
audio { border-radius: 8px !important; background: #161b22 !important; }
audio::-webkit-media-controls-panel { background: #161b22 !important; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #0d1117; }
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #484f58; }
"""
with gr.Blocks(title="Empathy Engine", css=CSS, theme=gr.themes.Base()) as demo:
gr.Markdown("""
# Empathy Engine
### Giving AI a Human Voice
Prepared for the **Darwix AI Evaluation** assignment
---
**K V Jaya Harsha** | [Personal Website](https://kvjharsha.vercel.app) | [GitHub](https://github.com/kvj-harsha) | [LinkedIn](https://linkedin.com/in/kvjharsha)
---
""")
with gr.Accordion("Two Approaches to the Problem", open=True):
gr.Markdown("""
## Approach 1 β€” Implemented (This Demo)
Uses the **Inworld TTS API** with dynamic vocal parameter modulation driven by a HuggingFace emotion classifier:
- **Emotion Detection** β€” `j-hartmann/emotion-english-distilroberta-base` β€” 7 classes: joy, surprise, anger, disgust, fear, sadness, neutral
- **Intensity Scaling** β€” Model confidence (0–1) linearly scales vocal params β€” "I'm okay" sounds different from "THIS IS AMAZING"
- **Vocal Modulation** β€” `speakingRate` (speed) + `temperature` (expressiveness) β€” both dynamically computed per emotion
- **Emotion Mapping** β€” Each emotion has a defined base rate + temperature, scaled by intensity score
- **TTS Engine** β€” Inworld TTS `inworld-tts-1.5-max`, voice: Clive, output: `.mp3`
---
## Approach 2 β€” Theoretical (Research-Grade)
Architecturally superior β€” validated in research, not implemented here due to time and compute constraints:
- **Model** β€” **F5-TTS** with conditional flow matching, learns the full distribution of human speech prosody
- **Emotion Transfer** β€” Zero-shot emotion cloning from reference audio β€” the model hears the target emotion and replicates it
- **Why it's better** β€” Flow matching captures micro-variations in pitch, rhythm, and timbre that API parameters cannot replicate
- **Papers** β€” F5-TTS: A Fairytale for Flow-matching-based TTS (Chen et al., 2024) Β· Voicebox (Meta AI, 2023)
> This approach remains theoretical in this submission but is well-validated in published research, and would be the production-grade choice given sufficient time and GPU resources.
""")
gr.Markdown("---")
with gr.Row():
with gr.Column(scale=3):
txt_input = gr.Textbox(
label="Input Text",
placeholder="Type something expressive... e.g. 'I cannot believe we actually pulled this off!'",
lines=4,
)
gr.Examples(examples=SAMPLES, inputs=txt_input, label="Quick Samples")
synth_btn = gr.Button("Synthesize Voice", variant="primary")
with gr.Column(scale=1):
gr.Markdown("""
**Emotion to Voice Mapping**
| Emotion | Rate | Temp |
|---|---|---|
| Joyful | 1.25–1.50 | 1.3–1.7 |
| Surprised | 1.20–1.40 | 1.4–1.75 |
| Fearful | 1.10–1.25 | 1.2–1.45 |
| Neutral | 1.00 | 1.00 |
| Angry | 0.75–0.85 | 0.35–0.50 |
| Disgusted | 0.70–0.80 | 0.45–0.55 |
| Sad | 0.60–0.70 | 0.35–0.45 |
*Rate = speed Β· Temp = energy*
""")
gr.Markdown("### Results")
with gr.Row():
with gr.Column():
audio_out = gr.Audio(label="Synthesized Audio", type="filepath", elem_classes="output-box")
emotion_out = gr.Textbox(label="Detected Emotion & Parameters", lines=5,
interactive=False, elem_classes="output-box")
with gr.Column():
stats_out = gr.Textbox(label="Session Stats", lines=4,
interactive=False, elem_classes="output-box")
dist_out = gr.Textbox(label="Emotion Distribution", lines=8,
interactive=False, elem_classes="output-box")
hist_out = gr.Textbox(label="Session History (last 10)", lines=10,
interactive=False, elem_classes="output-box")
with gr.Accordion("Requirements Coverage", open=False):
gr.Markdown("""
| # | Requirement | Status | Notes |
|---|---|---|---|
| 1 | Text Input | COMPLETE | Textbox + REST endpoint |
| 2 | Emotion Detection β€” 3+ classes | COMPLETE | 7 classes via DistilRoBERTa |
| 3 | Vocal Parameter Modulation β€” 2+ | COMPLETE | speakingRate + temperature |
| 4 | Emotion to Voice Mapping | COMPLETE | Config table with clear logic per emotion |
| 5 | Audio Output (.mp3) | COMPLETE | Inworld TTS, Base64 decoded to file |
| 6 | Granular Emotions | BONUS | 7 states: joy, anger, fear, sadness, surprise, disgust, neutral |
| 7 | Intensity Scaling | BONUS | Confidence score linearly scales both params |
| 8 | Web Interface | BONUS | Gradio UI with live stats, history, distribution |
""")
gr.Markdown("""
---
**Stack:** Python Β· Gradio Β· HuggingFace Transformers Β· Inworld TTS API Β· httpx
*Darwix AI Evaluation Β· 2025 Β· K V Jaya Harsha*
""")
synth_btn.click(
fn=synthesize,
inputs=[txt_input],
outputs=[audio_out, emotion_out, stats_out, dist_out, hist_out],
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)