DevTail0r commited on
Commit
fcc019e
Β·
verified Β·
1 Parent(s): ee36be0

Update services/podcast_service.py

Browse files
Files changed (1) hide show
  1. services/podcast_service.py +28 -68
services/podcast_service.py CHANGED
@@ -8,26 +8,20 @@ import uuid
8
  from datetime import datetime
9
  from pathlib import Path
10
 
11
- from huggingface_hub import InferenceClient
12
 
13
  from state import Artifact, Notebook
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
- GEN_MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
18
- MAX_NEW_TOKENS = 2048
19
- TEMPERATURE = 0.7
20
- TIMEOUT_SEC = 90
21
 
22
- # Two different HF TTS models for distinct host voices β€” runs via HF Inference API,
23
- # no local model download needed, works fine on free Spaces
24
- TTS_MODEL_ALEX = "espnet/kan-bayashi_ljspeech_vits" # Host Alex
25
- TTS_MODEL_SAM = "facebook/mms-tts-eng" # Host Sam
26
-
27
- # Max chars per TTS request
28
  TTS_CHUNK_CHARS = 300
29
 
30
- # Output dir for audio β€” must be accessible by Gradio's /file= endpoint
31
  AUDIO_OUTPUT_DIR = Path("assets/podcasts")
32
 
33
 
@@ -59,7 +53,6 @@ Now write the podcast script:"""
59
 
60
 
61
  def _parse_script_lines(raw: str) -> list[tuple[str, str]]:
62
- """Parse script into list of (speaker, text) tuples."""
63
  lines = []
64
  for line in raw.strip().splitlines():
65
  line = line.strip()
@@ -77,7 +70,6 @@ def _parse_script_lines(raw: str) -> list[tuple[str, str]]:
77
 
78
 
79
  def _script_to_markdown(lines: list[tuple[str, str]]) -> str:
80
- """Convert parsed lines to readable markdown for display."""
81
  parts = ["## πŸŽ™οΈ Podcast Script\n"]
82
  for speaker, text in lines:
83
  emoji = "🎀" if speaker == "Alex" else "🎧"
@@ -86,7 +78,6 @@ def _script_to_markdown(lines: list[tuple[str, str]]) -> str:
86
 
87
 
88
  def _chunk_text(text: str, max_chars: int = TTS_CHUNK_CHARS) -> list[str]:
89
- """Split text into chunks at sentence boundaries."""
90
  sentences = re.split(r'(?<=[.!?])\s+', text.strip())
91
  chunks, current = [], ""
92
  for sent in sentences:
@@ -106,55 +97,38 @@ def _chunk_text(text: str, max_chars: int = TTS_CHUNK_CHARS) -> list[str]:
106
 
107
 
108
  def _synthesize_audio(lines: list[tuple[str, str]], output_path: Path) -> bool:
109
- """
110
- Synthesize podcast audio using HF Inference API TTS.
111
- Calls the API per chunk, concatenates raw audio bytes, saves as WAV.
112
- Returns True on success, False on failure.
113
- """
114
  try:
115
  import numpy as np
116
  import scipy.io.wavfile as wav
 
117
 
118
  token = os.environ.get("HF_TOKEN")
119
- client = InferenceClient(token=token, timeout=TIMEOUT_SEC)
120
 
121
- SAMPLE_RATE = 22050 # Standard for most HF TTS models
122
  all_audio: list[np.ndarray] = []
123
  silence = np.zeros(int(SAMPLE_RATE * 0.5), dtype=np.float32)
124
 
125
  for speaker, text in lines:
126
  tts_model = TTS_MODEL_ALEX if speaker == "Alex" else TTS_MODEL_SAM
127
- chunks = _chunk_text(text)
128
-
129
- for chunk in chunks:
130
  try:
131
- # text_to_speech returns raw audio bytes
132
  audio_bytes = client.text_to_speech(chunk, model=tts_model)
133
-
134
- # Parse WAV bytes using scipy
135
  rate, data = wav.read(io.BytesIO(audio_bytes))
136
-
137
- # Normalize to float32
138
  if data.dtype == np.int16:
139
  audio_float = data.astype(np.float32) / 32768.0
140
  elif data.dtype == np.int32:
141
  audio_float = data.astype(np.float32) / 2147483648.0
142
  else:
143
  audio_float = data.astype(np.float32)
144
-
145
- # Handle stereo β†’ mono
146
  if audio_float.ndim == 2:
147
  audio_float = audio_float.mean(axis=1)
148
-
149
- # Resample if needed (simple approach: just use as-is, note the rate)
150
  if rate != SAMPLE_RATE:
151
- SAMPLE_RATE = rate # Use whatever rate the model returns
152
-
153
  all_audio.append(audio_float)
154
-
155
- except Exception as chunk_err:
156
- logger.warning("TTS chunk failed for %s: %s", speaker, chunk_err)
157
-
158
  all_audio.append(silence)
159
 
160
  if not all_audio:
@@ -171,7 +145,7 @@ def _synthesize_audio(lines: list[tuple[str, str]], output_path: Path) -> bool:
171
  return True
172
 
173
  except ImportError as e:
174
- logger.warning("scipy not installed, skipping TTS: %s", e)
175
  return False
176
  except Exception as e:
177
  logger.error("TTS synthesis failed: %s", e)
@@ -179,44 +153,30 @@ def _synthesize_audio(lines: list[tuple[str, str]], output_path: Path) -> bool:
179
 
180
 
181
  def generate_podcast(notebook: Notebook, summary_content: str) -> Artifact:
182
- """
183
- Generate a podcast artifact:
184
- 1. Generate two-host script via HF InferenceClient (Qwen)
185
- 2. Synthesize audio via HF Inference API TTS (no local models)
186
- Returns an Artifact with markdown script + audio_path if TTS succeeded.
187
- """
188
- token = os.environ.get("HF_TOKEN")
189
- client = InferenceClient(token=token, timeout=TIMEOUT_SEC)
190
-
191
- prompt = _build_podcast_prompt(summary_content)
192
 
193
  try:
194
- response = client.chat_completion(
195
- model=GEN_MODEL,
196
- messages=[
197
- {
198
- "role": "system",
199
- "content": (
200
- "You are a podcast scriptwriter. Write natural, engaging dialogue between two hosts. "
201
- "Every line must start with 'Alex:' or 'Sam:'. No markdown, no stage directions."
202
- ),
203
- },
204
- {"role": "user", "content": prompt},
205
- ],
206
- max_tokens=MAX_NEW_TOKENS,
207
- temperature=TEMPERATURE,
208
  )
209
- raw_script = response.choices[0].message.content or ""
210
  lines = _parse_script_lines(raw_script)
211
 
212
  if not lines:
213
- raise ValueError("No speaker lines parsed from LLM response.")
214
 
215
  except Exception as e:
216
  logger.error("Podcast script generation failed: %s", e)
217
  lines = [
218
  ("Alex", "Welcome to the podcast! Unfortunately we ran into an issue generating today's episode."),
219
- ("Sam", f"The script generation failed. Please try again."),
220
  ("Alex", "Thanks for listening β€” we'll be back soon!"),
221
  ]
222
 
 
8
  from datetime import datetime
9
  from pathlib import Path
10
 
11
+ import anthropic
12
 
13
  from state import Artifact, Notebook
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
+ MODEL = "claude-haiku-4-5-20251001"
18
+ MAX_TOKENS = 2048
 
 
19
 
20
+ # Two HF TTS models for distinct host voices β€” runs via HF Inference API
21
+ TTS_MODEL_ALEX = "espnet/kan-bayashi_ljspeech_vits"
22
+ TTS_MODEL_SAM = "facebook/mms-tts-eng"
 
 
 
23
  TTS_CHUNK_CHARS = 300
24
 
 
25
  AUDIO_OUTPUT_DIR = Path("assets/podcasts")
26
 
27
 
 
53
 
54
 
55
  def _parse_script_lines(raw: str) -> list[tuple[str, str]]:
 
56
  lines = []
57
  for line in raw.strip().splitlines():
58
  line = line.strip()
 
70
 
71
 
72
  def _script_to_markdown(lines: list[tuple[str, str]]) -> str:
 
73
  parts = ["## πŸŽ™οΈ Podcast Script\n"]
74
  for speaker, text in lines:
75
  emoji = "🎀" if speaker == "Alex" else "🎧"
 
78
 
79
 
80
  def _chunk_text(text: str, max_chars: int = TTS_CHUNK_CHARS) -> list[str]:
 
81
  sentences = re.split(r'(?<=[.!?])\s+', text.strip())
82
  chunks, current = [], ""
83
  for sent in sentences:
 
97
 
98
 
99
  def _synthesize_audio(lines: list[tuple[str, str]], output_path: Path) -> bool:
100
+ """Synthesize audio via HF Inference API TTS. Returns True on success."""
 
 
 
 
101
  try:
102
  import numpy as np
103
  import scipy.io.wavfile as wav
104
+ from huggingface_hub import InferenceClient
105
 
106
  token = os.environ.get("HF_TOKEN")
107
+ client = InferenceClient(token=token, timeout=90)
108
 
109
+ SAMPLE_RATE = 22050
110
  all_audio: list[np.ndarray] = []
111
  silence = np.zeros(int(SAMPLE_RATE * 0.5), dtype=np.float32)
112
 
113
  for speaker, text in lines:
114
  tts_model = TTS_MODEL_ALEX if speaker == "Alex" else TTS_MODEL_SAM
115
+ for chunk in _chunk_text(text):
 
 
116
  try:
 
117
  audio_bytes = client.text_to_speech(chunk, model=tts_model)
 
 
118
  rate, data = wav.read(io.BytesIO(audio_bytes))
 
 
119
  if data.dtype == np.int16:
120
  audio_float = data.astype(np.float32) / 32768.0
121
  elif data.dtype == np.int32:
122
  audio_float = data.astype(np.float32) / 2147483648.0
123
  else:
124
  audio_float = data.astype(np.float32)
 
 
125
  if audio_float.ndim == 2:
126
  audio_float = audio_float.mean(axis=1)
 
 
127
  if rate != SAMPLE_RATE:
128
+ SAMPLE_RATE = rate
 
129
  all_audio.append(audio_float)
130
+ except Exception as e:
131
+ logger.warning("TTS chunk failed for %s: %s", speaker, e)
 
 
132
  all_audio.append(silence)
133
 
134
  if not all_audio:
 
145
  return True
146
 
147
  except ImportError as e:
148
+ logger.warning("Missing dependency for TTS: %s", e)
149
  return False
150
  except Exception as e:
151
  logger.error("TTS synthesis failed: %s", e)
 
153
 
154
 
155
  def generate_podcast(notebook: Notebook, summary_content: str) -> Artifact:
156
+ """Generate podcast script via Claude + audio via HF TTS."""
157
+ client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
 
 
 
 
 
 
 
 
158
 
159
  try:
160
+ response = client.messages.create(
161
+ model=MODEL,
162
+ max_tokens=MAX_TOKENS,
163
+ system=(
164
+ "You are a podcast scriptwriter. Write natural, engaging dialogue between two hosts. "
165
+ "Every line must start with 'Alex:' or 'Sam:'. No markdown, no stage directions."
166
+ ),
167
+ messages=[{"role": "user", "content": _build_podcast_prompt(summary_content)}],
 
 
 
 
 
 
168
  )
169
+ raw_script = response.content[0].text or ""
170
  lines = _parse_script_lines(raw_script)
171
 
172
  if not lines:
173
+ raise ValueError("No speaker lines parsed from response.")
174
 
175
  except Exception as e:
176
  logger.error("Podcast script generation failed: %s", e)
177
  lines = [
178
  ("Alex", "Welcome to the podcast! Unfortunately we ran into an issue generating today's episode."),
179
+ ("Sam", "The script generation failed. Please try again."),
180
  ("Alex", "Thanks for listening β€” we'll be back soon!"),
181
  ]
182