DevTail0r commited on
Commit
e735a6a
·
verified ·
1 Parent(s): a00eac2

Update services/podcast_service.py

Browse files
Files changed (1) hide show
  1. services/podcast_service.py +55 -35
services/podcast_service.py CHANGED
@@ -1,9 +1,9 @@
1
  from __future__ import annotations
2
 
 
3
  import logging
4
  import os
5
  import re
6
- import tempfile
7
  import uuid
8
  from datetime import datetime
9
  from pathlib import Path
@@ -19,14 +19,15 @@ MAX_NEW_TOKENS = 2048
19
  TEMPERATURE = 0.7
20
  TIMEOUT_SEC = 90
21
 
22
- # Bark voice presets for the two hosts
23
- HOST_A_VOICE = "v2/en_speaker_6" # deeper male voice
24
- HOST_B_VOICE = "v2/en_speaker_9" # female voice
 
25
 
26
- # Max chars per Bark chunk (Bark struggles with long inputs)
27
- BARK_CHUNK_CHARS = 220
28
 
29
- # Output dir for audio files — must be accessible by Gradio
30
  AUDIO_OUTPUT_DIR = Path("assets/podcasts")
31
 
32
 
@@ -42,7 +43,7 @@ Write a podcast conversation between two hosts: Alex and Sam.
42
  - Sam explains concepts clearly with examples and analogies
43
  - The conversation should feel natural, not like a lecture
44
  - Cover the key points from the summary
45
- - Length: 10–15 exchanges (back-and-forth lines)
46
 
47
  FORMAT RULES (strictly follow these):
48
  - Every line must start with either "Alex:" or "Sam:" followed by a space
@@ -84,18 +85,16 @@ def _script_to_markdown(lines: list[tuple[str, str]]) -> str:
84
  return "\n".join(parts)
85
 
86
 
87
- def _chunk_text(text: str, max_chars: int) -> list[str]:
88
- """Split text into chunks at sentence boundaries for Bark TTS."""
89
  sentences = re.split(r'(?<=[.!?])\s+', text.strip())
90
- chunks = []
91
- current = ""
92
  for sent in sentences:
93
  if len(current) + len(sent) + 1 <= max_chars:
94
  current = (current + " " + sent).strip()
95
  else:
96
  if current:
97
  chunks.append(current)
98
- # If single sentence is too long, hard-split it
99
  if len(sent) > max_chars:
100
  for i in range(0, len(sent), max_chars):
101
  chunks.append(sent[i:i + max_chars])
@@ -108,37 +107,60 @@ def _chunk_text(text: str, max_chars: int) -> list[str]:
108
 
109
  def _synthesize_audio(lines: list[tuple[str, str]], output_path: Path) -> bool:
110
  """
111
- Synthesize podcast audio using Bark TTS.
112
- Concatenates per-speaker audio chunks into a single WAV file.
113
  Returns True on success, False on failure.
114
  """
115
  try:
116
  import numpy as np
117
- from bark import SAMPLE_RATE, generate_audio, preload_models
118
  import scipy.io.wavfile as wav
119
 
120
- logger.info("Loading Bark models...")
121
- preload_models()
122
 
 
123
  all_audio: list[np.ndarray] = []
124
- silence = np.zeros(int(SAMPLE_RATE * 0.4), dtype=np.float32) # 0.4s pause between turns
125
 
126
  for speaker, text in lines:
127
- voice_preset = HOST_A_VOICE if speaker == "Alex" else HOST_B_VOICE
128
- chunks = _chunk_text(text, BARK_CHUNK_CHARS)
 
129
  for chunk in chunks:
130
  try:
131
- audio_array = generate_audio(chunk, history_prompt=voice_preset)
132
- all_audio.append(audio_array.astype(np.float32))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  except Exception as chunk_err:
134
- logger.warning("Bark chunk failed for speaker %s: %s", speaker, chunk_err)
 
135
  all_audio.append(silence)
136
 
137
  if not all_audio:
138
  return False
139
 
140
  combined = np.concatenate(all_audio)
141
- # Normalize to prevent clipping
142
  max_val = np.max(np.abs(combined))
143
  if max_val > 0:
144
  combined = combined / max_val * 0.95
@@ -148,20 +170,20 @@ def _synthesize_audio(lines: list[tuple[str, str]], output_path: Path) -> bool:
148
  logger.info("Podcast audio saved to %s", output_path)
149
  return True
150
 
151
- except ImportError:
152
- logger.warning("Bark not installed. Skipping TTS. Install with: pip install bark")
153
  return False
154
  except Exception as e:
155
- logger.error("Bark TTS failed: %s", e)
156
  return False
157
 
158
 
159
  def generate_podcast(notebook: Notebook, summary_content: str) -> Artifact:
160
  """
161
  Generate a podcast artifact:
162
- 1. Generate script via HF InferenceClient
163
- 2. Synthesize audio via Bark TTS
164
- Returns an Artifact with content=markdown script and audio_path set if TTS succeeded.
165
  """
166
  token = os.environ.get("HF_TOKEN")
167
  client = InferenceClient(token=token, timeout=TIMEOUT_SEC)
@@ -194,14 +216,12 @@ def generate_podcast(notebook: Notebook, summary_content: str) -> Artifact:
194
  logger.error("Podcast script generation failed: %s", e)
195
  lines = [
196
  ("Alex", "Welcome to the podcast! Unfortunately we ran into an issue generating today's episode."),
197
- ("Sam", f"The script generation failed with error: {e}. Please try again."),
198
- ("Alex", "Thanks for listening, and we'll be back with a full episode soon!"),
199
  ]
200
 
201
- # Build markdown script for display
202
  markdown_script = _script_to_markdown(lines)
203
 
204
- # Synthesize audio
205
  audio_filename = f"podcast_{uuid.uuid4().hex[:12]}.wav"
206
  audio_path = AUDIO_OUTPUT_DIR / audio_filename
207
  audio_success = _synthesize_audio(lines, audio_path)
 
1
  from __future__ import annotations
2
 
3
+ import io
4
  import logging
5
  import os
6
  import re
 
7
  import uuid
8
  from datetime import datetime
9
  from pathlib import Path
 
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
 
 
43
  - Sam explains concepts clearly with examples and analogies
44
  - The conversation should feel natural, not like a lecture
45
  - Cover the key points from the summary
46
+ - Length: 10-15 exchanges (back-and-forth lines)
47
 
48
  FORMAT RULES (strictly follow these):
49
  - Every line must start with either "Alex:" or "Sam:" followed by a space
 
85
  return "\n".join(parts)
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:
93
  if len(current) + len(sent) + 1 <= max_chars:
94
  current = (current + " " + sent).strip()
95
  else:
96
  if current:
97
  chunks.append(current)
 
98
  if len(sent) > max_chars:
99
  for i in range(0, len(sent), max_chars):
100
  chunks.append(sent[i:i + max_chars])
 
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:
161
  return False
162
 
163
  combined = np.concatenate(all_audio)
 
164
  max_val = np.max(np.abs(combined))
165
  if max_val > 0:
166
  combined = combined / max_val * 0.95
 
170
  logger.info("Podcast audio saved to %s", output_path)
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)
178
  return False
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)
 
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
 
 
223
  markdown_script = _script_to_markdown(lines)
224
 
 
225
  audio_filename = f"podcast_{uuid.uuid4().hex[:12]}.wav"
226
  audio_path = AUDIO_OUTPUT_DIR / audio_filename
227
  audio_success = _synthesize_audio(lines, audio_path)