github-actions[bot] commited on
Commit
1815b1f
·
1 Parent(s): e457a17

Sync from GitHub 49159d9b28d28aa11280480ca7ae1daa166891aa

Browse files
app.py CHANGED
@@ -914,7 +914,28 @@ def _run_podcast_background(
914
  topic_focus=topic_focus,
915
  )
916
  if "error" in result:
917
- crud.update_artifact(db, artifact_id, status="failed", error_message=result["error"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  else:
919
  transcript_markdown = generator.format_transcript_markdown(result)
920
  transcript_path = generator.save_transcript(result, str(user_id), str(notebook_id))
 
914
  topic_focus=topic_focus,
915
  )
916
  if "error" in result:
917
+ transcript_markdown = ""
918
+ transcript_path = None
919
+ transcript = result.get("transcript")
920
+ if isinstance(transcript, list) and transcript:
921
+ transcript_markdown = generator.format_transcript_markdown(result)
922
+ transcript_path = generator.save_transcript(result, str(user_id), str(notebook_id))
923
+ crud.update_artifact(
924
+ db,
925
+ artifact_id,
926
+ status="failed",
927
+ error_message=result["error"],
928
+ content=(transcript_markdown or None),
929
+ metadata={
930
+ "audio_path": None,
931
+ "transcript_path": transcript_path,
932
+ **(
933
+ result.get("metadata", {})
934
+ if isinstance(result.get("metadata"), dict)
935
+ else {}
936
+ ),
937
+ },
938
+ )
939
  else:
940
  transcript_markdown = generator.format_transcript_markdown(result)
941
  transcript_path = generator.save_transcript(result, str(user_id), str(notebook_id))
src/artifacts/podcast_generator.py CHANGED
@@ -142,10 +142,50 @@ class PodcastGenerator:
142
  # 3. Synthesize audio segments
143
  print(f"🎵 Synthesizing audio with {self.tts_provider}...")
144
  audio_segments = self._synthesize_segments(script, user_id, notebook_id, hosts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
  # 4. Combine audio
147
  print("🔗 Combining audio segments...")
148
  final_audio = self._combine_audio(audio_segments, user_id, notebook_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
  return {
151
  "transcript": script,
 
142
  # 3. Synthesize audio segments
143
  print(f"🎵 Synthesizing audio with {self.tts_provider}...")
144
  audio_segments = self._synthesize_segments(script, user_id, notebook_id, hosts)
145
+ if not audio_segments:
146
+ return {
147
+ "error": (
148
+ "Transcript generated but audio synthesis failed for all segments. "
149
+ "Check TTS provider credentials, quota, and configured voices."
150
+ ),
151
+ "transcript": script,
152
+ "audio_path": None,
153
+ "metadata": {
154
+ "notebook_id": notebook_id,
155
+ "duration_target": duration_target,
156
+ "hosts": hosts,
157
+ "tts_provider": self.tts_provider,
158
+ "llm_provider": self.llm_provider,
159
+ "llm_model": self.model,
160
+ "num_segments": len(script),
161
+ "topic_focus": topic_focus,
162
+ "generated_at": datetime.utcnow().isoformat(),
163
+ },
164
+ }
165
 
166
  # 4. Combine audio
167
  print("🔗 Combining audio segments...")
168
  final_audio = self._combine_audio(audio_segments, user_id, notebook_id)
169
+ if not final_audio or not Path(final_audio).exists():
170
+ return {
171
+ "error": (
172
+ "Transcript generated but final audio file was not created. "
173
+ "Check ffmpeg/pydub setup and TTS output."
174
+ ),
175
+ "transcript": script,
176
+ "audio_path": None,
177
+ "metadata": {
178
+ "notebook_id": notebook_id,
179
+ "duration_target": duration_target,
180
+ "hosts": hosts,
181
+ "tts_provider": self.tts_provider,
182
+ "llm_provider": self.llm_provider,
183
+ "llm_model": self.model,
184
+ "num_segments": len(script),
185
+ "topic_focus": topic_focus,
186
+ "generated_at": datetime.utcnow().isoformat(),
187
+ },
188
+ }
189
 
190
  return {
191
  "transcript": script,
tests/test_artifact_api.py CHANGED
@@ -20,6 +20,7 @@ sys.path.insert(0, str(ROOT))
20
 
21
  from data.db import Base, get_db
22
  from data import crud
 
23
  from app import app
24
 
25
 
@@ -246,6 +247,45 @@ class TestPodcastEndpoint:
246
  data = resp.json()
247
  assert data["metadata"]["topic_focus"] == "neural nets"
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
  # ── List artifacts tests ──────────────────────────────────────────────────────
251
 
 
20
 
21
  from data.db import Base, get_db
22
  from data import crud
23
+ import app as app_module
24
  from app import app
25
 
26
 
 
247
  data = resp.json()
248
  assert data["metadata"]["topic_focus"] == "neural nets"
249
 
250
+ def test_background_podcast_failure_persists_transcript(self, notebook, db_session):
251
+ """If audio fails but transcript exists, artifact should be failed with transcript content."""
252
+ artifact = crud.create_artifact(
253
+ db=db_session,
254
+ notebook_id=notebook.id,
255
+ artifact_type="podcast",
256
+ metadata={"duration": "5min"},
257
+ )
258
+
259
+ result_payload = {
260
+ "error": "Transcript generated but audio synthesis failed for all segments.",
261
+ "transcript": [{"speaker": "Alex", "text": "Intro text"}],
262
+ "audio_path": None,
263
+ "metadata": {"tts_provider": "elevenlabs"},
264
+ }
265
+
266
+ with patch("app.PodcastGenerator") as MockGen, patch("app.SessionLocal", return_value=db_session):
267
+ mock_gen = MagicMock()
268
+ mock_gen.generate_podcast.return_value = result_payload
269
+ mock_gen.format_transcript_markdown.return_value = "# Podcast Transcript\n\n**Alex:** Intro text"
270
+ mock_gen.save_transcript.return_value = "/tmp/transcript.md"
271
+ MockGen.return_value = mock_gen
272
+
273
+ app_module._run_podcast_background(
274
+ artifact_id=artifact.id,
275
+ user_id=1,
276
+ notebook_id=notebook.id,
277
+ duration="5min",
278
+ topic_focus=None,
279
+ )
280
+
281
+ updated = crud.get_artifact(db_session, artifact.id)
282
+ assert updated is not None
283
+ assert updated.status == "failed"
284
+ assert updated.content is not None
285
+ assert "Podcast Transcript" in updated.content
286
+ assert updated.error_message is not None
287
+ assert "audio synthesis failed" in updated.error_message.lower()
288
+
289
 
290
  # ── List artifacts tests ──────────────────────────────────────────────────────
291
 
tests/test_artifacts.py CHANGED
@@ -192,6 +192,7 @@ class TestPodcastGenerator:
192
  env = {
193
  "STORAGE_BASE_DIR": str(tmp_path / "data"),
194
  "OPENAI_API_KEY": "test-key",
 
195
  "TTS_PROVIDER": "edge",
196
  **(extra_env or {}),
197
  }
@@ -209,10 +210,12 @@ class TestPodcastGenerator:
209
  mock_llm_resp = _make_openai_chat_response(MOCK_PODCAST_LLM_RESPONSE)
210
 
211
  fake_audio = str(tmp_path / "podcast.mp3")
 
212
 
213
  env = {
214
  "STORAGE_BASE_DIR": str(tmp_path / "data"),
215
  "OPENAI_API_KEY": "test-key",
 
216
  "TTS_PROVIDER": "edge",
217
  }
218
  with patch.dict(os.environ, env):
@@ -245,6 +248,7 @@ class TestPodcastGenerator:
245
  env = {
246
  "STORAGE_BASE_DIR": str(tmp_path / "nonexistent"),
247
  "OPENAI_API_KEY": "test-key",
 
248
  "TTS_PROVIDER": "edge",
249
  }
250
  with patch.dict(os.environ, env):
@@ -266,6 +270,7 @@ class TestPodcastGenerator:
266
  env = {
267
  "STORAGE_BASE_DIR": str(tmp_path / "data"),
268
  "OPENAI_API_KEY": "test-key",
 
269
  "TTS_PROVIDER": "edge",
270
  }
271
  with patch.dict(os.environ, env):
@@ -287,7 +292,11 @@ class TestPodcastGenerator:
287
  "metadata": {"duration_target": "5min"},
288
  }
289
 
290
- env = {"OPENAI_API_KEY": "test-key", "TTS_PROVIDER": "edge"}
 
 
 
 
291
  with patch.dict(os.environ, env):
292
  with patch("src.artifacts.tts_adapter.EdgeTTS"):
293
  with patch("src.artifacts.podcast_generator.OpenAI"):
@@ -314,6 +323,7 @@ class TestPodcastGenerator:
314
  env = {
315
  "STORAGE_BASE_DIR": str(tmp_path / "data"),
316
  "OPENAI_API_KEY": "test-key",
 
317
  "TTS_PROVIDER": "edge",
318
  }
319
  with patch.dict(os.environ, env):
@@ -336,3 +346,36 @@ class TestPodcastGenerator:
336
  )
337
 
338
  assert result["metadata"]["topic_focus"] == "neural networks"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  env = {
193
  "STORAGE_BASE_DIR": str(tmp_path / "data"),
194
  "OPENAI_API_KEY": "test-key",
195
+ "TRANSCRIPT_LLM_PROVIDER": "openai",
196
  "TTS_PROVIDER": "edge",
197
  **(extra_env or {}),
198
  }
 
210
  mock_llm_resp = _make_openai_chat_response(MOCK_PODCAST_LLM_RESPONSE)
211
 
212
  fake_audio = str(tmp_path / "podcast.mp3")
213
+ pathlib.Path(fake_audio).write_bytes(b"fake-audio")
214
 
215
  env = {
216
  "STORAGE_BASE_DIR": str(tmp_path / "data"),
217
  "OPENAI_API_KEY": "test-key",
218
+ "TRANSCRIPT_LLM_PROVIDER": "openai",
219
  "TTS_PROVIDER": "edge",
220
  }
221
  with patch.dict(os.environ, env):
 
248
  env = {
249
  "STORAGE_BASE_DIR": str(tmp_path / "nonexistent"),
250
  "OPENAI_API_KEY": "test-key",
251
+ "TRANSCRIPT_LLM_PROVIDER": "openai",
252
  "TTS_PROVIDER": "edge",
253
  }
254
  with patch.dict(os.environ, env):
 
270
  env = {
271
  "STORAGE_BASE_DIR": str(tmp_path / "data"),
272
  "OPENAI_API_KEY": "test-key",
273
+ "TRANSCRIPT_LLM_PROVIDER": "openai",
274
  "TTS_PROVIDER": "edge",
275
  }
276
  with patch.dict(os.environ, env):
 
292
  "metadata": {"duration_target": "5min"},
293
  }
294
 
295
+ env = {
296
+ "OPENAI_API_KEY": "test-key",
297
+ "TRANSCRIPT_LLM_PROVIDER": "openai",
298
+ "TTS_PROVIDER": "edge",
299
+ }
300
  with patch.dict(os.environ, env):
301
  with patch("src.artifacts.tts_adapter.EdgeTTS"):
302
  with patch("src.artifacts.podcast_generator.OpenAI"):
 
323
  env = {
324
  "STORAGE_BASE_DIR": str(tmp_path / "data"),
325
  "OPENAI_API_KEY": "test-key",
326
+ "TRANSCRIPT_LLM_PROVIDER": "openai",
327
  "TTS_PROVIDER": "edge",
328
  }
329
  with patch.dict(os.environ, env):
 
346
  )
347
 
348
  assert result["metadata"]["topic_focus"] == "neural networks"
349
+
350
+ def test_generate_podcast_when_tts_fails_returns_error_with_transcript(self, tmp_path):
351
+ """If TTS produces no audio segments, generator returns an explicit error."""
352
+ _chroma_dir(tmp_path)
353
+
354
+ mock_store = MagicMock()
355
+ mock_store.query.return_value = MOCK_CHROMA_RESULTS
356
+ mock_llm_resp = _make_openai_chat_response(MOCK_PODCAST_LLM_RESPONSE)
357
+
358
+ env = {
359
+ "STORAGE_BASE_DIR": str(tmp_path / "data"),
360
+ "OPENAI_API_KEY": "test-key",
361
+ "TRANSCRIPT_LLM_PROVIDER": "openai",
362
+ "TTS_PROVIDER": "edge",
363
+ }
364
+ with patch.dict(os.environ, env):
365
+ with patch("src.artifacts.tts_adapter.EdgeTTS"):
366
+ with patch(
367
+ "src.artifacts.podcast_generator.ChromaAdapter", return_value=mock_store
368
+ ):
369
+ with patch("src.artifacts.podcast_generator.OpenAI") as mock_openai_cls:
370
+ mock_client = MagicMock()
371
+ mock_client.chat.completions.create.return_value = mock_llm_resp
372
+ mock_openai_cls.return_value = mock_client
373
+
374
+ gen = PodcastGenerator()
375
+ with patch.object(gen, "_synthesize_segments", return_value=[]):
376
+ result = gen.generate_podcast(user_id="1", notebook_id="1")
377
+
378
+ assert "error" in result
379
+ assert "audio synthesis failed" in str(result["error"]).lower()
380
+ assert isinstance(result.get("transcript"), list)
381
+ assert len(result["transcript"]) > 0
tests/test_podcast_llm_providers.py CHANGED
@@ -45,8 +45,10 @@ def test_podcast_generator_ollama_provider_without_openai_key(tmp_path):
45
 
46
  with patch("src.artifacts.podcast_generator.requests.post", return_value=mock_resp):
47
  generator = PodcastGenerator(llm_provider="ollama")
48
- with patch.object(generator, "_synthesize_segments", return_value=[]):
49
- with patch.object(generator, "_combine_audio", return_value=""):
 
 
50
  result = generator.generate_podcast("1", "1")
51
 
52
  assert "error" not in result
@@ -84,8 +86,10 @@ def test_podcast_generator_groq_provider_without_openai_key(tmp_path):
84
  mock_groq_cls.return_value = mock_groq
85
 
86
  generator = PodcastGenerator(llm_provider="groq")
87
- with patch.object(generator, "_synthesize_segments", return_value=[]):
88
- with patch.object(generator, "_combine_audio", return_value=""):
 
 
89
  result = generator.generate_podcast("1", "1")
90
 
91
  assert "error" not in result
 
45
 
46
  with patch("src.artifacts.podcast_generator.requests.post", return_value=mock_resp):
47
  generator = PodcastGenerator(llm_provider="ollama")
48
+ fake_audio = str(tmp_path / "ollama_podcast.mp3")
49
+ pathlib.Path(fake_audio).write_bytes(b"audio")
50
+ with patch.object(generator, "_synthesize_segments", return_value=[fake_audio]):
51
+ with patch.object(generator, "_combine_audio", return_value=fake_audio):
52
  result = generator.generate_podcast("1", "1")
53
 
54
  assert "error" not in result
 
86
  mock_groq_cls.return_value = mock_groq
87
 
88
  generator = PodcastGenerator(llm_provider="groq")
89
+ fake_audio = str(tmp_path / "groq_podcast.mp3")
90
+ pathlib.Path(fake_audio).write_bytes(b"audio")
91
+ with patch.object(generator, "_synthesize_segments", return_value=[fake_audio]):
92
+ with patch.object(generator, "_combine_audio", return_value=fake_audio):
93
  result = generator.generate_podcast("1", "1")
94
 
95
  assert "error" not in result