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."
    )