Move caller dialog to Sonnet 4.6, add SearXNG for Devon, and fix stale model ids
Working-tree changes that had accumulated without being committed. The test updates matter most: tests/test_caller_gen.py was left behind when caller_gen started requiring voice and age in regulars_included, so the committed tree had a failing suite that only passed locally. - Caller dialog moves from Haiku 4.5 to Sonnet 4.6 (~$1/show to ~$3-4/show) - Grok pinned to x-ai/grok-4.3; grok-4, grok-4-fast and grok-4.1-fast were retired from OpenRouter, and llm.py swallows the 404 and returns empty text, so a retired id makes callers go silent with nothing in the logs - Devon's web_search now runs against SearXNG on the NAS - Assorted TTS, audio, news, cost tracker and control-panel changes - CLAUDE.md updated to match Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1344,14 +1344,26 @@ class AudioService:
|
||||
if self._monitor_write:
|
||||
self._monitor_write(indata[:, record_channel].copy())
|
||||
|
||||
self._stem_mic_stream = sd.InputStream(
|
||||
device=self.input_device,
|
||||
channels=max_channels,
|
||||
samplerate=device_sr,
|
||||
dtype=np.float32,
|
||||
blocksize=1024,
|
||||
callback=callback,
|
||||
)
|
||||
def _open():
|
||||
return sd.InputStream(
|
||||
device=self.input_device,
|
||||
channels=max_channels,
|
||||
samplerate=device_sr,
|
||||
dtype=np.float32,
|
||||
blocksize=1024,
|
||||
callback=callback,
|
||||
)
|
||||
|
||||
try:
|
||||
self._stem_mic_stream = _open()
|
||||
except Exception as first_err:
|
||||
print(f"[StemRecorder] InputStream open failed ({first_err}), refreshing PortAudio and retrying...")
|
||||
self._refresh_devices()
|
||||
device_info = sd.query_devices(self.input_device)
|
||||
max_channels = device_info['max_input_channels']
|
||||
device_sr = int(device_info['default_samplerate'])
|
||||
self._stem_mic_stream = _open()
|
||||
|
||||
self._stem_mic_stream.start()
|
||||
print(f"[StemRecorder] Host mic capture started (device {self.input_device} ch {self.input_channel} @ {device_sr}Hz)")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
import json
|
||||
import random
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -51,23 +52,48 @@ def parse_batch_response(raw: str) -> list[CallerIdentity]:
|
||||
|
||||
def resolve_voice(suggestion: str, roster: list[str]) -> str:
|
||||
"""Map sonnet's voice suggestion to a real voice in the roster.
|
||||
Case-insensitive exact match; deterministic fallback to first roster entry."""
|
||||
if not suggestion or not roster:
|
||||
return roster[0] if roster else ""
|
||||
lower_map = {v.lower(): v for v in roster}
|
||||
return lower_map.get(suggestion.lower(), roster[0])
|
||||
Case-insensitive exact match; random fallback if unmatched (so LLM
|
||||
hallucinations don't all collapse onto roster[0])."""
|
||||
if not roster:
|
||||
return ""
|
||||
if suggestion:
|
||||
match = {v.lower(): v for v in roster}.get(suggestion.lower())
|
||||
if match:
|
||||
return match
|
||||
fallback = random.choice(roster)
|
||||
print(f"[caller_gen] voice '{suggestion}' not in roster — random fallback to '{fallback}'")
|
||||
return fallback
|
||||
return random.choice(roster)
|
||||
|
||||
|
||||
BATCH_SYSTEM_PROMPT = """You are writing a roster of callers for Luke's late-night radio show in New Mexico.
|
||||
BATCH_SYSTEM_PROMPT = """You are writing a roster of callers for Luke's late-night radio show, broadcast out of Alpine, Texas, in the Big Bend country of far West Texas.
|
||||
|
||||
WHERE THESE CALLERS LIVE: The show's world is the Big Bend region of far West Texas — high desert, remote, dark skies, ranching country up against the Mexico border. Most callers are from or know this area. Ground them in REAL places and facts only — never invent businesses or landmarks that don't exist. The towns:
|
||||
- Alpine — the hub of the region (about 6,000 people), Brewster County seat, home of Sul Ross State University, a mile-high old ranching and railroad town with a small arts scene. The show broadcasts from here.
|
||||
- Marfa — minimalist-art tourist town (the Chinati Foundation / Donald Judd, the Prada Marfa installation), famous for the unexplained Marfa Lights. Old ranching families chafing against an influx of artists and out-of-towners.
|
||||
- Marathon — tiny, known for the historic Gage Hotel, the eastern gateway to Big Bend National Park.
|
||||
- Terlingua — a quicksilver-mining ghost town turned off-grid haven for desert eccentrics; famous for its chili cookoff, river-rafting outfitters on the Rio Grande, and the Starlight Theatre. Right up against the Mexico border (Boquillas crossing).
|
||||
- Fort Stockton — an oilfield and I-10 town to the north in Pecos County (Paisano Pete the roadrunner), more working-class and blue-collar than the artsy towns.
|
||||
- The Big Bend itself — the Chisos Mountains, the Rio Grande, some of the darkest night skies in the country (the McDonald Observatory is near Fort Davis), brutally remote, with Permian Basin oil money booming to the north.
|
||||
Callers can reference real ranches, the heat and wind, the drive times (everything is hours apart), border life, Sul Ross students, oilfield work, tourists, and the desert weirdos — but keep it real and specific to this place.
|
||||
|
||||
CREATIVE RANGE: Your callers must span the emotional range of Howard Stern (chaos, strong characters), Coast to Coast AM (earnest weirdos, sincere believers), Loveline (real problems, real advice-seeking), Delilah (emotional vulnerability, connection), and Opie and Anthony (sharp, irreverent, specific people).
|
||||
|
||||
ROSTER MIX — VARY THE CALL TYPES. A great show is not ten people in moral crisis. Build the roster of 10 callers roughly like this:
|
||||
- 4-5 DILEMMA CALLS (the dramatic spine): real human conflict with STAKES — moral dilemmas, confessions, betrayals, impossible choices, or something the caller did and can't take back. Think: "I found out my dad has a second family," "I got someone fired and they deserved it but now their kid is sick," "my best friend's wife hit on me and I didn't say no." These need genuine emotional weight.
|
||||
- 2-3 STORY / ENTHUSIAST CALLS (the relief): callers with a wild thing that happened to them, a fascinating obsession or piece of knowledge, a strange-but-true story, or a vivid slice of life. NO deep moral dilemma required — they call because the story is great, the fact is amazing, or they're bursting to talk about the thing they love. Still specific, still a reason they called TONIGHT, but the energy is delight or wonder or a great yarn, not anguish.
|
||||
- 1-2 TRUE BELIEVER / CHAOS CALLS (the spice): an earnest, sincere weirdo — UFOs, cryptids, a government conspiracy, a pattern only they can see, a paranormal experience (Coast to Coast AM energy, dead serious about it) — OR a big eccentric personality on a tear about something trivial. Played straight, never winking.
|
||||
|
||||
Every caller still needs SOMETHING that makes the audience lean in — a problem, a secret, a story, a wild belief, or an irresistible enthusiasm. But not every caller carries grief. Let the show breathe.
|
||||
|
||||
Maximum character distance between callers. No two callers should feel like siblings.
|
||||
|
||||
ANTI-COLLISION RULE — THIS IS NON-NEGOTIABLE: All callers in this roster must be clearly differentiated. No two callers may share the same hobby, obsession, profession archetype, or story theme. Specifically forbidden within a single roster: two BBQ competitors, two taxidermists, two amateur-radio or mystery-signal callers, two callers with ex-spouse drama, two conspiracy/pattern-seers, two retired military, two grandmothers-of-many, two callers calling about a weird neighbor, two callers with religious-object stories. Each caller's defining "thing" — their hook, their obsession, the specific topic they're calling about — must appear exactly once in the roster. Before finalizing, scan your output and swap any collisions.
|
||||
ANTI-COLLISION RULE — THIS IS NON-NEGOTIABLE: All callers in this roster must be clearly differentiated. No two callers may share the same hobby, obsession, profession archetype, or story theme. Specifically forbidden within a single roster: two BBQ competitors, two taxidermists, two amateur-radio or mystery-signal callers, two callers with ex-spouse drama, two believers chasing the SAME phenomenon (e.g. two UFO callers, or two cryptid callers — if you have two believer calls they must be about completely different things), two retired military, two grandmothers-of-many, two callers calling about a weird neighbor, two callers with religious-object stories. Each caller's defining "thing" — their hook, their obsession, the specific topic they're calling about — must appear exactly once in the roster. Before finalizing, scan your output and swap any collisions.
|
||||
|
||||
Do not default to sitcom plots. Real humans are specific and strange. Give each caller details that could only belong to them.
|
||||
|
||||
OPENING LINE RULES: Each caller's opening_line must be unique and specific to their situation. NEVER write "I've been listening for X years" or "long-time listener, first-time caller" or any variant. The caller should jump into their story or problem immediately — nervous, excited, angry, whatever fits. The opening line is the hook that makes the audience lean in.
|
||||
|
||||
You will output strict JSON with a "callers" array. Each caller has exactly these fields: name, age, voice_suggestion, location, identity, situation, reason_calling, opening_line, secret_want, specific_details (array of 2-3 strings), emotional_register."""
|
||||
|
||||
|
||||
@@ -81,6 +107,19 @@ def build_batch_prompt(ctx: dict) -> str:
|
||||
lines.append(f"- {h}")
|
||||
lines.append("")
|
||||
|
||||
theme = (ctx.get("theme") or "").strip()
|
||||
if theme:
|
||||
lines.append(f'TONIGHT\'S SHOW THEME: "{theme}"')
|
||||
lines.append(
|
||||
f'Roughly 2/3 of callers MUST be calling BECAUSE OF this theme — the theme '
|
||||
f'should be woven directly into their reason_calling and situation, not '
|
||||
f'just acknowledged. Make the connection specific and personal (a story, a '
|
||||
f'conflict, a moment) not abstract. The remaining 1/3 of the roster can be '
|
||||
f'unrelated walk-ins for variety. Do NOT make every caller theme-connected — '
|
||||
f'variety still matters.'
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
if ctx["recent_caller_summaries"]:
|
||||
lines.append("Recent callers (DO NOT repeat these archetypes or situations):")
|
||||
for s in ctx["recent_caller_summaries"]:
|
||||
|
||||
@@ -37,10 +37,15 @@ class TTSCallRecord:
|
||||
OPENROUTER_PRICING = {
|
||||
# Claude
|
||||
"anthropic/claude-sonnet-4.6": {"prompt": 3.00, "completion": 15.00},
|
||||
"anthropic/claude-sonnet-4-5": {"prompt": 3.00, "completion": 15.00},
|
||||
"anthropic/claude-sonnet-4.5": {"prompt": 3.00, "completion": 15.00},
|
||||
"anthropic/claude-sonnet-4-5": {"prompt": 3.00, "completion": 15.00}, # retired id, historical
|
||||
"anthropic/claude-haiku-4.5": {"prompt": 0.80, "completion": 4.00},
|
||||
"anthropic/claude-3-haiku": {"prompt": 0.25, "completion": 1.25},
|
||||
# Grok
|
||||
"x-ai/grok-4.3": {"prompt": 1.25, "completion": 2.50},
|
||||
"x-ai/grok-4.5": {"prompt": 2.00, "completion": 6.00},
|
||||
"x-ai/grok-4.20": {"prompt": 1.25, "completion": 2.50},
|
||||
# Retired on OpenRouter — kept so historical records stay costable
|
||||
"x-ai/grok-4.1-fast": {"prompt": 0.20, "completion": 0.50},
|
||||
"x-ai/grok-4": {"prompt": 3.00, "completion": 15.00},
|
||||
"x-ai/grok-4-fast": {"prompt": 5.00, "completion": 15.00},
|
||||
@@ -60,6 +65,7 @@ OPENROUTER_PRICING = {
|
||||
"google/gemini-flash-1.5": {"prompt": 0.075, "completion": 0.30},
|
||||
# Meta
|
||||
"meta-llama/llama-3.3-70b-instruct": {"prompt": 0.10, "completion": 0.32},
|
||||
"meta-llama/llama-3.1-8b-instruct": {"prompt": 0.05, "completion": 0.08},
|
||||
"meta-llama/llama-4-maverick": {"prompt": 0.20, "completion": 0.60},
|
||||
# Other
|
||||
"moonshotai/kimi-k2": {"prompt": 0.60, "completion": 2.00},
|
||||
|
||||
@@ -21,7 +21,7 @@ INTERN_SYSTEM_PROMPT = """You are Devon, the 23-year-old intern on "Luke at the
|
||||
|
||||
YOUR ROLE: You're the show's researcher and general assistant. You look things up, fact-check claims, pull up information when asked, and occasionally interject with relevant facts or opinions. You do NOT host. You do NOT screen calls. You sit in the booth and try to be useful.
|
||||
|
||||
YOUR BACKGROUND: Communications degree from NMSU. You've been interning here for a few weeks now. You drive a 2009 Civic with a permanent check engine light. You live in a studio in Deming. You take this job seriously even though nobody else seems to take you seriously. You are actively looking for a girlfriend and you are not subtle about it. You see your position at the radio station as a way to meet women — you'll try to impress female callers, ask if they're single, suggest they come by the studio, or find excuses to keep them on the line longer. Luke hates this. You have a deeply strange personal history that you reference casually — a complicated sexual past, weird childhood experiences, inexplicable life choices. SOMETIMES (maybe 1 in 3 responses, not every time) you'll mention one of these things unprompted like it's completely normal. "Yeah that reminds me of when my ex and I got stuck in that storage unit for a whole weekend — anyway, it says here that..." The rest of the time you just answer the question or react normally without a personal callback.
|
||||
YOUR BACKGROUND: Communications degree from NMSU. You've been interning here for a few weeks now. You drive a 2009 Civic with a permanent check engine light. You live in a studio in Alpine, Texas — you moved out to the Big Bend with the show. You take this job seriously even though nobody else seems to take you seriously. You are actively looking for a girlfriend and you are not subtle about it. You see your position at the radio station as a way to meet women — you'll try to impress female callers, ask if they're single, suggest they come by the studio, or find excuses to keep them on the line longer. Luke hates this. You have a deeply strange personal history that you reference casually — a complicated sexual past, weird childhood experiences, inexplicable life choices. SOMETIMES (maybe 1 in 3 responses, not every time) you'll mention one of these things unprompted like it's completely normal. "Yeah that reminds me of when my ex and I got stuck in that storage unit for a whole weekend — anyway, it says here that..." The rest of the time you just answer the question or react normally without a personal callback.
|
||||
|
||||
YOUR PERSONALITY:
|
||||
- You are a weird little dude. Kinda creepy, very funny, awkward, and surprisingly sharp. You give off a vibe that something is slightly off about you but people can't quite place it. But underneath it all, you are genuinely lovable. You have a good heart. You root for people. You get excited for callers. You care about the show. People should hear you and think "this guy is insane" and also "I love this guy." You are the kind of person who is impossible not to root for even when you're being deeply strange.
|
||||
|
||||
@@ -12,33 +12,32 @@ from .cost_tracker import cost_tracker
|
||||
OPENROUTER_MODELS = [
|
||||
# Primary
|
||||
"anthropic/claude-sonnet-4.6",
|
||||
"x-ai/grok-4.1-fast",
|
||||
"x-ai/grok-4",
|
||||
"x-ai/grok-4.3",
|
||||
"x-ai/grok-4.5",
|
||||
# Style-matched pool
|
||||
"mistralai/mistral-large-2512",
|
||||
"deepseek/deepseek-r1-distill-llama-70b",
|
||||
"meta-llama/llama-3.3-70b-instruct",
|
||||
"google/gemini-2.5-flash",
|
||||
# Other good options
|
||||
"anthropic/claude-sonnet-4-5",
|
||||
"anthropic/claude-sonnet-4.5",
|
||||
"anthropic/claude-haiku-4.5",
|
||||
"deepseek/deepseek-chat-v3-0324",
|
||||
"mistralai/mistral-small-2603",
|
||||
"google/gemini-2.5-pro",
|
||||
"google/gemini-3-flash-preview",
|
||||
"x-ai/grok-4-fast",
|
||||
"x-ai/grok-4.20",
|
||||
"moonshotai/kimi-k2",
|
||||
"qwen/qwen3-235b-a22b",
|
||||
"meta-llama/llama-4-maverick",
|
||||
# Legacy
|
||||
"anthropic/claude-3-haiku",
|
||||
"google/gemini-flash-1.5",
|
||||
"meta-llama/llama-3.1-8b-instruct",
|
||||
]
|
||||
|
||||
# Fast models to try as fallbacks (cheap, fast, good enough for conversation)
|
||||
FALLBACK_MODELS = [
|
||||
"mistralai/mistral-small-creative",
|
||||
"mistralai/mistral-small-2603",
|
||||
"google/gemini-2.5-flash",
|
||||
"openai/gpt-4o-mini",
|
||||
]
|
||||
@@ -332,8 +331,8 @@ class LLMService:
|
||||
# - Grok/Mistral/DeepSeek/Kimi: slightly warmer than Sonnet defaults
|
||||
_CALLER_DIALOG_MODEL_PARAMS = {
|
||||
"anthropic/claude-sonnet-4.6": {"temperature": 0.65, "frequency_penalty": 0.3, "presence_penalty": 0.15},
|
||||
"x-ai/grok-4.1-fast": {"temperature": 0.7, "frequency_penalty": 0.2, "presence_penalty": 0.1},
|
||||
"x-ai/grok-4": {"temperature": 0.7, "frequency_penalty": 0.2, "presence_penalty": 0.1},
|
||||
"x-ai/grok-4.3": {"temperature": 0.7, "frequency_penalty": 0.2, "presence_penalty": 0.1},
|
||||
"x-ai/grok-4.5": {"temperature": 0.7, "frequency_penalty": 0.2, "presence_penalty": 0.1},
|
||||
"qwen/qwen3-235b-a22b": {"temperature": 0.6, "frequency_penalty": 0.5, "presence_penalty": 0.2},
|
||||
"mistralai/mistral-large-2512": {"temperature": 0.7, "frequency_penalty": 0.2, "presence_penalty": 0.1},
|
||||
"deepseek/deepseek-chat-v3-0324": {"temperature": 0.7, "frequency_penalty": 0.2, "presence_penalty": 0.1},
|
||||
|
||||
@@ -7,7 +7,9 @@ from dataclasses import dataclass
|
||||
|
||||
import httpx
|
||||
|
||||
SEARXNG_URL = "http://localhost:8888"
|
||||
from ..config import settings
|
||||
|
||||
SEARXNG_URL = settings.searxng_url
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -39,6 +39,16 @@ class RegularCallerService:
|
||||
def get_regulars(self) -> list[dict]:
|
||||
return list(self._regulars)
|
||||
|
||||
def get_by_name(self, name: str) -> Optional[dict]:
|
||||
"""Find a regular by name (case-insensitive)."""
|
||||
if not name:
|
||||
return None
|
||||
target = name.lower()
|
||||
for r in self._regulars:
|
||||
if r.get("name", "").lower() == target:
|
||||
return r
|
||||
return None
|
||||
|
||||
def get_returning_callers(self, count: int = 2) -> list[dict]:
|
||||
"""Get up to `count` regulars for returning caller slots"""
|
||||
import random
|
||||
|
||||
+31
-9
@@ -83,14 +83,18 @@ VITS_SPEAKERS = {
|
||||
DEFAULT_VITS_SPEAKER = "p225"
|
||||
|
||||
# Inworld voice mapping - maps ElevenLabs voice IDs to Inworld voices
|
||||
# Full voice list from API (English): Abby, Alex, Amina, Anjali, Arjun, Ashley,
|
||||
# Blake, Brian, Callum, Carter, Celeste, Chloe, Claire, Clive, Craig, Darlene,
|
||||
# Deborah, Dennis, Derek, Dominus, Edward, Elizabeth, Elliot, Ethan, Evan, Evelyn,
|
||||
# Gareth, Graham, Grant, Hades, Hamish, Hana, Hank, Jake, James, Jason, Jessica,
|
||||
# Julia, Kayla, Kelsey, Lauren, Liam, Loretta, Luna, Malcolm, Mark, Marlene,
|
||||
# Miranda, Mortimer, Nate, Oliver, Olivia, Pippa, Pixie, Priya, Ronald, Rupert,
|
||||
# Saanvi, Sarah, Sebastian, Serena, Shaun, Simon, Snik, Tessa, Theodore, Timothy,
|
||||
# Tyler, Veronica, Victor, Victoria, Vinny, Wendy
|
||||
# Full voice list from API (English, as of 2026-05): Abby, Alex, Amina, Anjali,
|
||||
# Arjun, Ashley, Avery, Bianca, Blake, Brandon, Brian, Callum, Carter, Cedric,
|
||||
# Celeste, Chloe, Claire, Clive, Conrad, Craig, Damon, Darlene, Deborah, Dennis,
|
||||
# Derek, Dominus, Duncan, Edward, Eleanor, Elizabeth, Elliot, Ethan, Evan,
|
||||
# Evelyn, Felix, Gareth, Graham, Grant, Hades, Hamish, Hana, Hank, Jake, James,
|
||||
# Jason, Jessica, Jonah, Julia, Kayla, Kelsey, Lauren, Levi, Liam, Loretta,
|
||||
# Lucian, Luna, Malcolm, Marcus, Mark, Marlene, Mia, Miranda, Mortimer, Nadia,
|
||||
# Naomi, Nate, Oliver, Olivia, Pippa, Pixie, Priya, Reed, Riley, Ronald, Rupert,
|
||||
# Saanvi, Sarah, Sebastian, Selene, Serena, Shaun, Simon, Snik, Sophie, Tessa,
|
||||
# Theodore, Timothy, Trevor, Tristan, Tyler, Veronica, Victor, Victoria, Vinny,
|
||||
# Wendy. Not in our caller pool: Abby/Mia/Pixie/Riley (child voices), Dominus/
|
||||
# Lucian/Selene/Snik (theatrical), Dominus/Hades blacklisted.
|
||||
INWORLD_VOICES = {
|
||||
# Original voice IDs
|
||||
"VR6AewLTigWG4xSOukaG": "Edward", # Tony - fast-talking, emphatic, streetwise
|
||||
@@ -161,11 +165,18 @@ VOICE_PROFILES = {
|
||||
"Elliot": {"weight": "light", "energy": "medium", "warmth": "warm", "age_feel": "young"}, # used by Otis (comedian)
|
||||
# Remaining male pool voices
|
||||
"Arjun": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"},
|
||||
"Avery": {"weight": "light", "energy": "high", "warmth": "warm", "age_feel": "young"}, # youthful, performative, gameshow host
|
||||
"Brandon": {"weight": "medium", "energy": "high", "warmth": "neutral", "age_feel": "middle"}, # bold, strident, news-style
|
||||
"Brian": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"},
|
||||
"Callum": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "young"},
|
||||
"Cedric": {"weight": "medium", "energy": "low", "warmth": "cool", "age_feel": "mature"}, # crisp, measured, formal announcements
|
||||
"Conrad": {"weight": "heavy", "energy": "low", "warmth": "cool", "age_feel": "mature"}, # gruff, weathered detective
|
||||
"Damon": {"weight": "medium", "energy": "low", "warmth": "cool", "age_feel": "middle"}, # calm, raspy, atmospheric
|
||||
"Derek": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "middle"},
|
||||
"Duncan": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"}, # warm, articulate British
|
||||
"Ethan": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "young"},
|
||||
"Evan": {"weight": "light", "energy": "medium", "warmth": "neutral", "age_feel": "young"},
|
||||
"Felix": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"}, # calm, friendly British
|
||||
"Gareth": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "middle"},
|
||||
"Graham": {"weight": "heavy", "energy": "low", "warmth": "neutral", "age_feel": "mature"},
|
||||
"Grant": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "middle"},
|
||||
@@ -175,13 +186,19 @@ VOICE_PROFILES = {
|
||||
"Jake": {"weight": "medium", "energy": "high", "warmth": "warm", "age_feel": "young"},
|
||||
"James": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "middle"},
|
||||
"Jason": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "middle"},
|
||||
"Jonah": {"weight": "medium", "energy": "low", "warmth": "warm", "age_feel": "middle"}, # soothing, calm, reassuring
|
||||
"Levi": {"weight": "heavy", "energy": "low", "warmth": "cool", "age_feel": "middle"}, # measured, ominous suspense
|
||||
"Liam": {"weight": "medium", "energy": "high", "warmth": "warm", "age_feel": "young"},
|
||||
"Malcolm": {"weight": "heavy", "energy": "low", "warmth": "cool", "age_feel": "mature"},
|
||||
"Marcus": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"}, # authoritative, empathetic
|
||||
"Mortimer": {"weight": "heavy", "energy": "low", "warmth": "cool", "age_feel": "mature"},
|
||||
"Nate": {"weight": "light", "energy": "high", "warmth": "warm", "age_feel": "young"},
|
||||
"Oliver": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"},
|
||||
"Reed": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "middle"}, # clear, professional American
|
||||
"Rupert": {"weight": "medium", "energy": "low", "warmth": "cool", "age_feel": "mature"},
|
||||
"Simon": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "middle"},
|
||||
"Trevor": {"weight": "medium", "energy": "high", "warmth": "neutral", "age_feel": "middle"}, # punchy, expressive, energetic promos
|
||||
"Tristan": {"weight": "medium", "energy": "low", "warmth": "neutral", "age_feel": "middle"}, # deliberate, controlled, documentary
|
||||
"Tyler": {"weight": "light", "energy": "high", "warmth": "neutral", "age_feel": "young"},
|
||||
"Victor": {"weight": "heavy", "energy": "medium", "warmth": "cool", "age_feel": "mature"},
|
||||
"Vinny": {"weight": "medium", "energy": "high", "warmth": "warm", "age_feel": "middle"},
|
||||
@@ -200,10 +217,12 @@ VOICE_PROFILES = {
|
||||
"Kelsey": {"weight": "light", "energy": "medium", "warmth": "neutral", "age_feel": "young"}, # used by Maxine (quiet/nervous)
|
||||
# Remaining female pool voices
|
||||
"Anjali": {"weight": "light", "energy": "medium", "warmth": "warm", "age_feel": "young"},
|
||||
"Bianca": {"weight": "medium", "energy": "low", "warmth": "cool", "age_feel": "middle"}, # deep, controlled corporate
|
||||
"Celeste": {"weight": "light", "energy": "medium", "warmth": "cool", "age_feel": "middle"},
|
||||
"Chloe": {"weight": "light", "energy": "high", "warmth": "warm", "age_feel": "young"},
|
||||
"Claire": {"weight": "medium", "energy": "medium", "warmth": "neutral", "age_feel": "middle"},
|
||||
"Darlene": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "mature"},
|
||||
"Eleanor": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"}, # polished, approachable British
|
||||
"Elizabeth": {"weight": "medium", "energy": "medium", "warmth": "cool", "age_feel": "mature"},
|
||||
"Jessica": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"},
|
||||
"Kayla": {"weight": "light", "energy": "high", "warmth": "warm", "age_feel": "young"},
|
||||
@@ -212,9 +231,12 @@ VOICE_PROFILES = {
|
||||
"Luna": {"weight": "light", "energy": "medium", "warmth": "warm", "age_feel": "young"},
|
||||
"Marlene": {"weight": "medium", "energy": "low", "warmth": "neutral", "age_feel": "mature"},
|
||||
"Miranda": {"weight": "medium", "energy": "medium", "warmth": "cool", "age_feel": "middle"},
|
||||
"Nadia": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"}, # personable, lively
|
||||
"Naomi": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"}, # warm, grounded narrative
|
||||
"Pippa": {"weight": "light", "energy": "high", "warmth": "warm", "age_feel": "young"},
|
||||
"Saanvi": {"weight": "light", "energy": "medium", "warmth": "warm", "age_feel": "young"},
|
||||
"Serena": {"weight": "medium", "energy": "medium", "warmth": "cool", "age_feel": "middle"},
|
||||
"Sophie": {"weight": "medium", "energy": "medium", "warmth": "warm", "age_feel": "middle"}, # friendly British
|
||||
"Veronica": {"weight": "medium", "energy": "medium", "warmth": "cool", "age_feel": "middle"},
|
||||
"Victoria": {"weight": "medium", "energy": "low", "warmth": "cool", "age_feel": "mature"},
|
||||
}
|
||||
@@ -771,7 +793,7 @@ async def generate_speech_inworld(text: str, voice_id: str, emotional_register:
|
||||
payload = {
|
||||
"text": text,
|
||||
"voiceId": voice,
|
||||
"modelId": "inworld-tts-1.5-max",
|
||||
"modelId": "inworld-tts-2",
|
||||
"temperature": temperature,
|
||||
"applyTextNormalization": "ON",
|
||||
"audioConfig": {
|
||||
|
||||
Reference in New Issue
Block a user