Spaces:
Sleeping
Sleeping
File size: 11,464 Bytes
dd8a76e e431ba2 dd8a76e d94b032 dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e e431ba2 12c6d8d e431ba2 dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d dd8a76e 12c6d8d 80c8a75 dd8a76e 12c6d8d 80c8a75 12c6d8d dd8a76e 80c8a75 dd8a76e 80c8a75 dd8a76e 12c6d8d 80c8a75 dd8a76e 12c6d8d dd8a76e 80c8a75 12c6d8d dd8a76e 80c8a75 d94b032 dd8a76e 12c6d8d 80c8a75 12c6d8d dd8a76e d94b032 80c8a75 12c6d8d dd8a76e 80c8a75 dd8a76e 80c8a75 dd8a76e e431ba2 dd8a76e 80c8a75 dd8a76e e431ba2 dd8a76e e431ba2 dd8a76e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | # script_gen.py β AI Script Generation
#
# Primary: Groq (Llama 4 Maverick) β 1,000 req/day free, no credit card
# Fallback: Google Gemini 2.0 Flash β ~20 req/day free, no credit card
from groq import Groq
import google.genai as genai
from google.genai import types as genai_types
# ββ Tone modifiers injected directly into system prompts βββββββββββββββββββββ
# Tone must live in the SYSTEM prompt β not the user message β because the LLM
# treats system prompt instructions as its core identity. Tone in the user
# message gets overridden by the system prompt's established style.
TONE_SYSTEM_MODIFIER = {
"Professional": (
"TONE: Formal and authoritative. Use precise, clear language. "
"No slang, no casual expressions. Every sentence should feel considered and expert."
),
"Casual": (
"TONE: Relaxed and conversational. Write like you're chatting with a friend. "
"Use everyday expressions, contractions, and a warm, approachable vibe. "
"Avoid stiff or formal phrasing."
),
"Dramatic": (
"TONE: Expressive and emotive. Build tension. Emphasise stakes. "
"Use vivid, punchy language. Pause for effect. Make the audience feel something. "
"Every sentence should have weight and urgency."
),
"Humorous": (
"TONE: Witty and playful. Weave in jokes, puns, and light-hearted observations "
"based on the content. Keep it fun without being silly. "
"Surprise the listener with unexpected but relevant humour. "
"A well-placed joke should land every 3-4 exchanges."
),
"Educational": (
"TONE: Patient and instructive. Write as if explaining to someone encountering "
"this topic for the first time. Define terms immediately. Use simple analogies. "
"Build understanding step by step. Never assume prior knowledge."
),
}
TONE_USER_REMINDER = {
"Professional": "Maintain formal, authoritative language throughout. No exceptions.",
"Casual": "Keep it conversational and friendly all the way through β contractions, warmth, zero stiffness.",
"Dramatic": "Stay dramatic β every line should carry tension, weight, or urgency. Don't let it go flat.",
"Humorous": "Stay playful and witty throughout. Include jokes or puns naturally woven into the content.",
"Educational": "Stay instructive and clear throughout β always explain, never assume.",
}
# ββ Base system prompts (tone injected dynamically) βββββββββββββββββββββββββββ
BASE_SYSTEM_PROMPTS = {
"podcast": """{tone_modifier}
You are a professional podcast scriptwriter.
Write a natural, engaging 2-host podcast script using ONLY the provided source material.
Format EVERY spoken line strictly as one of:
ALEX: <what Alex says>
JAMIE: <what Jamie says>
Structure:
- ALEX opens with a hook and introduces the topic (2-3 lines)
- Both hosts discuss 3-4 key points from the material, alternating naturally
- Include moments of reaction, curiosity, and follow-up questions between hosts
- JAMIE delivers a clear summary of key takeaways (2-3 lines)
- ALEX closes with a sign-off (1-2 lines)
Rules:
- Natural spoken English β no bullet points in the dialogue
- No markdown, no headers, no asterisks in the output
- Every single line must start with ALEX: or JAMIE:
- Ground all content strictly in the source material provided
- Apply the tone above consistently to BOTH hosts' voices""",
"debate": """{tone_modifier}
You are a professional debate scriptwriter.
Write a structured, passionate debate using ONLY the provided source material.
Format EVERY spoken line strictly as one of:
PRO: <argument>
CON: <counterargument>
Structure:
- PRO opening statement (2-3 lines)
- CON opening statement (2-3 lines)
- Round 1: PRO argument β CON rebuttal
- Round 2: PRO argument β CON rebuttal
- Round 3: PRO argument β CON rebuttal
- PRO closing statement
- CON closing statement
Rules:
- Each line should be 2-4 sentences long
- Arguments must be grounded in the source material
- Every line must start with PRO: or CON:
- Apply the tone above consistently β it should colour how each side argues""",
"storytelling": """{tone_modifier}
You are a master storyteller and narrator.
Transform the source material into a compelling audio narrative.
Format EVERY line strictly as:
NARRATOR: <narration text>
Structure:
- Open with a vivid hook drawn directly from the source material
- Use analogies and relatable comparisons to explain concepts in the source
- Build a story arc using ONLY what is in the source: setup β complexity β insight
- Close with a reflection based strictly on what the source concludes
Rules:
- Vivid spoken English β no bullet points, no lists, no markdown
- Every line must start with NARRATOR:
- Do NOT invent background, history, or origin stories not in the source
- Apply the tone above to the narrator's voice and style throughout""",
"lecture": """{tone_modifier}
You are an engaging university professor.
Deliver a clear, structured lecture using ONLY the provided source material.
Format EVERY spoken line strictly as:
PROFESSOR: <lecture content>
Structure:
- Open with a compelling hook or real-world question
- Introduce core concepts one by one with explanations and examples
- Use analogies to make abstract ideas concrete
- Summarise key learnings at the end
- Close with a thought-provoking question or next step
Rules:
- Explain jargon immediately when used
- No bullet points or lists in the speech itself
- Every line must start with PROFESSOR:
- Apply the tone above to how the professor speaks throughout""",
"poem": """{tone_modifier}
You are a gifted spoken word poet.
Transform the source material into a beautiful, evocative poem meant to be read aloud.
Every image, idea, and emotion must come directly from the source β no invented content.
Format EVERY line strictly as:
NARRATOR: <poem line>
Structure:
- Opening stanza (4-6 lines): A vivid, arresting image or idea drawn from the source
- Body stanzas (3-4 stanzas of 4-6 lines each): Explore the key ideas poetically β
use metaphor, rhythm, and imagery to illuminate what the source material means
- Closing stanza (4-6 lines): A resonant, memorable conclusion grounded in the source
Rules:
- Write in free verse or with a natural rhyme scheme β never forced rhymes
- Every line must start with NARRATOR:
- Use line breaks deliberately β short lines for emphasis, longer lines for flow
- Favour concrete images over abstract statements
- Do NOT invent facts, history, or context not present in the source
- The poem should feel like it was written by a human poet, not generated
- Apply the tone above to the voice and emotional register of the poem""",
}
def build_system_prompt(mode: str, tone: str) -> str:
"""Build a system prompt with tone baked directly into it."""
base = BASE_SYSTEM_PROMPTS.get(mode, BASE_SYSTEM_PROMPTS["podcast"])
modifier = TONE_SYSTEM_MODIFIER.get(tone, TONE_SYSTEM_MODIFIER["Professional"])
return base.format(tone_modifier=modifier)
USER_PROMPT_TEMPLATE = """Create a complete, production-ready {mode} script based STRICTLY on the source material below.
Duration target: Approximately {duration} minute(s) of spoken audio.
Word count target: Aim for approximately {target_words} words total.
(Spoken audio averages ~130 words per minute)
TONE REMINDER: {tone_reminder}
Critical rules:
- Do NOT add any facts, claims, or details not explicitly present in the source
- Keep all content grounded and accurate to the source
- Write in natural spoken English β this will be converted to audio
- Hit the word count target as closely as possible
- Follow the format exactly as instructed (every line must have the correct speaker label)
SOURCE MATERIAL:
{context}
Write the complete script now."""
# ββ Provider functions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def generate_with_groq(context: str, mode: str, api_key: str,
tone: str = "Professional", duration: float = 2.0) -> str:
client = Groq(api_key=api_key)
target_words = int(duration * 130)
max_tokens = min(4096, max(1024, int(target_words * 1.6)))
tone_reminder = TONE_USER_REMINDER.get(tone, TONE_USER_REMINDER["Professional"])
system_prompt = build_system_prompt(mode, tone)
response = client.chat.completions.create(
model="meta-llama/llama-4-maverick-17b-128e-instruct",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": USER_PROMPT_TEMPLATE.format(
mode=mode, context=context,
duration=duration, target_words=target_words,
tone_reminder=tone_reminder,
)},
],
max_tokens=max_tokens,
temperature=0.75, # slightly higher for tone variety
)
result = response.choices[0].message.content
if not result or not result.strip():
raise ValueError("Groq returned an empty response.")
return result
def generate_with_gemini(context: str, mode: str, api_key: str,
tone: str = "Professional", duration: float = 2.0) -> str:
client = genai.Client(api_key=api_key)
target_words = int(duration * 130)
max_tokens = min(4096, max(1024, int(target_words * 1.6)))
tone_reminder = TONE_USER_REMINDER.get(tone, TONE_USER_REMINDER["Professional"])
system_prompt = build_system_prompt(mode, tone)
full_prompt = f"{system_prompt}\n\n{USER_PROMPT_TEMPLATE.format(mode=mode, context=context, duration=duration, target_words=target_words, tone_reminder=tone_reminder)}"
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=full_prompt,
config=genai_types.GenerateContentConfig(
max_output_tokens=max_tokens,
temperature=0.75,
),
)
result = response.text
if not result or not result.strip():
raise ValueError("Gemini returned an empty response.")
return result
# ββ Main entry point ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def generate_script(
context: str,
mode: str,
groq_key: str = "",
gemini_key: str = "",
tone: str = "Professional",
duration: float = 2.0,
) -> str:
errors = []
if groq_key.strip():
try:
return generate_with_groq(context, mode, groq_key.strip(), tone, duration)
except Exception as e:
errors.append(f"Groq error: {str(e)}")
print(f"[VoiceVerse] Groq failed, trying Gemini. Reason: {e}")
if gemini_key.strip():
try:
return generate_with_gemini(context, mode, gemini_key.strip(), tone, duration)
except Exception as e:
errors.append(f"Gemini error: {str(e)}")
print(f"[VoiceVerse] Gemini also failed. Reason: {e}")
raise ValueError(
"Script generation failed.\n\n"
+ "\n".join(errors)
+ "\n\nPlease check that your API keys are correctly set in HF Space Secrets."
) |