Coverage for wrapper/podcasttranscript/wrapper_podcasttranscript.py: 44%
369 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
1"""
2Podcast transcript generation wrapper.
3It uses an LLM to generate podcast transcripts from PDF documents.
4"""
6import asyncio
7import logging
8import requests
9import re
10import traceback
11import json
12import tempfile
13import aiofiles
15from azure.identity import DefaultAzureCredential
16from azure.identity import get_bearer_token_provider
17from openai import AsyncAzureOpenAI
18from openai import AsyncOpenAI
20from typing import TYPE_CHECKING
21from typing import cast
22from typing import override
23from typing import List
24from typing import Dict
25from typing import Optional
26from typing import Any
27from typing import AsyncGenerator
29if TYPE_CHECKING:
30 from openai import AsyncStream
31 from openai.types.chat import ChatCompletionChunk
33from pydantic import BaseModel
34from pydantic import Field
35from tenacity import retry
36from tenacity import stop_after_attempt
37from tenacity import wait_random_exponential
39from wrapper_model import ModelGeneration
41from file_utils import base64_to_binary
42from media_utils import fix_json_like_string
44from pdf_utils import parse_pdf
46from transcript_prompts import QUESTION_MODIFIER
47from transcript_prompts import SYSTEM_PROMPT
48from transcript_prompts import IMG_STYLE_MODIFIERS
49from transcript_prompts import IMG_SCENE_MODIFIERS
51from http import HTTPStatus
54DEFAULT_MAX_TOKENS = 5000
55DEFAULT_MAX_DIALOGUES = 50
56DEFAULT_NUM_CHARACTERS = 2
57DEFAULT_TEMPERATURE = 0.7
60class Dialogue(BaseModel):
61 character: str = Field(
62 ..., description="Name of the character delivering the dialogue."
63 )
64 transcript: str = Field(
65 ..., description="Transcript of the dialogue. Must not be more than 50 words."
66 )
67 end_script: bool = Field(
68 default=False,
69 description="Indicates if the script is finished. If True, no more dialogues will be generated."
70 )
72 def __str__(self) -> str:
73 str_ = f"{self.character}: {self.transcript}"
74 return str_
77class Script(BaseModel):
78 dialogues: List[Dialogue] = Field(
79 ...,
80 description="Ordered list of dialogues in the script."
81 )
83 def __str__(self) -> str:
84 str_ = "Script:\n\n"
85 for dialogue in self.dialogues:
86 str_ += f"{dialogue.__str__()}\n\n"
87 return str_
90class Scene(BaseModel):
91 characters: List[str] = Field(
92 ..., description="List of characters shown in this scene."
93 )
94 dialogues: List[Dialogue] = Field(
95 ..., description="Ordered list of dialogues in the scene."
96 )
98 def __str__(self) -> str:
99 str_ = f"Characters: {self.characters}\n\n"
100 str_ += "Transcript:\n\n"
101 for dialogue in self.dialogues:
102 str_ += f"{dialogue.__str__()}\n\n"
103 return str_
106class Podcast(BaseModel):
107 scenes: List[Scene] = Field(
108 ..., description="Ordered list of scenes in the podcast."
109 )
111 def __str__(self) -> str:
112 str_ = ""
113 for scene_idx, scene in enumerate(self.scenes):
114 str_ += f"Scene: {scene_idx}\n{scene.__str__()}"
115 return str_
118class PodcastTranscriptGenerator(ModelGeneration):
119 """
120 A class to generate podcast transcripts from PDF documents using an LLM.
121 This class handles downloading PDFs, parsing them into text and images,
122 and generating a structured podcast script with scenes and dialogues.
123 """
125 def __init__(
126 self,
127 llm_url: str = "http://localhost:8000/v1",
128 llm_model: str = "meta-llama/Meta-Llama-3.1-8B",
129 multi_modal: bool = False,
130 ) -> None:
131 super().__init__("podcasttranscript")
132 self.set_llm(llm_model, llm_url, multi_modal)
134 def load_model(self) -> None:
135 logging.debug("No model for podcast transcript generation.")
137 def init_model_parallelism(self) -> None:
138 logging.debug("No parallelism for podcast transcript generation.")
140 def model_compile(self) -> None:
141 logging.debug("No compilation for podcast transcript generation.")
143 def _get_azure_openai_client(self, base_url: str) -> AsyncAzureOpenAI:
144 def _get_azure_api_version(base_url: str) -> Optional[str]:
145 api_version_pattern = r"api-version=([^&]+)"
146 match = re.search(api_version_pattern, base_url)
147 if match:
148 return match.group(1)
149 logging.error("Failed to extract API version from URL")
150 return None
152 api_version = _get_azure_api_version(base_url)
153 azure_token_url = "https://cognitiveservices.azure.com/.default"
154 token_provider = get_bearer_token_provider(
155 DefaultAzureCredential(), azure_token_url
156 )
157 return AsyncAzureOpenAI(
158 azure_ad_token_provider=token_provider,
159 azure_endpoint=base_url,
160 api_version=api_version)
162 def set_llm(
163 self,
164 llm_model: str,
165 llm_url: str,
166 multi_modal: bool = False,
167 api_key: str = "n/a",
168 ) -> None:
169 self.llm_model = llm_model
170 self.llm_url = llm_url
171 self.multi_modal = multi_modal
172 self.llm_client: AsyncOpenAI
173 if "azure" in llm_url:
174 self.llm_client = self._get_azure_openai_client(llm_url)
175 self.extra_body = None
176 else:
177 self.llm_client = AsyncOpenAI(
178 api_key=api_key,
179 base_url=self.llm_url)
180 # timeout=10.0) # Set a timeout for LLM requests
181 self.extra_body = dict(guided_decoding_backend="xgrammar")
183 def download_pdf(
184 self,
185 url: str,
186 job_id: Optional[str] = None,
187 ) -> str:
188 """
189 Download the PDF from the given URL and save it to a temporary file.
190 """
191 response = requests.get(url, timeout=30)
192 if response.status_code == HTTPStatus.OK:
193 if not job_id:
194 output_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name
195 else:
196 output_path = f"/tmp/{job_id}.pdf"
197 with open(output_path, "wb") as file:
198 file.write(response.content)
199 return output_path
200 raise Exception(f"Failed to download PDF, status code: {response.status_code}")
202 async def gen_script(
203 self,
204 pdf_images: List[str],
205 pdf_text: List[str],
206 max_tokens: int = DEFAULT_MAX_TOKENS,
207 temperature: float = DEFAULT_TEMPERATURE,
208 max_dialogues: int = DEFAULT_MAX_DIALOGUES,
209 max_words_per_dialogue: int = -1,
210 job_id: Optional[str] = None,
211 ) -> Script:
212 QUESTION = "What is the main theme of this paper?"
214 # Prepare the user prompt text string
215 user_prompt_text = "\n\n".join(pdf_text)
216 user_prompt_text += f"\n\n{QUESTION_MODIFIER} {QUESTION}"
218 # Add images to the user prompt
219 user_prompt: List[Dict[str, Any]] = [{"type": "text", "text": user_prompt_text}]
220 if self.multi_modal:
221 for image_url in pdf_images:
222 user_prompt.append({"type": "image_url", "image_url": {"url": image_url}})
224 # Prepare the system prompt text string
225 system_prompt = SYSTEM_PROMPT
226 system_prompt += f"\n\n{QUESTION_MODIFIER} {QUESTION}"
228 # Combine the prompts into a single message
229 messages: List[Dict[str, Any]] = [
230 {"role": "system", "content": system_prompt},
231 {"role": "user", "content": user_prompt}
232 ]
234 async with aiofiles.open(f"/tmp/{job_id}_prompt.json", "w") as prompt_file:
235 await prompt_file.write(json.dumps(messages, indent=2))
237 try:
238 response = await self.llm_client.beta.chat.completions.parse(
239 model=self.llm_model,
240 messages=messages, # type: ignore[arg-type]
241 temperature=temperature,
242 max_tokens=max_tokens,
243 extra_body=self.extra_body,
244 response_format=Script,
245 )
246 logging.info(f"LLM response: {response}")
247 except Exception as ex:
248 msg = f"Cannot query LLM for script at {self.llm_url}: {ex}."
249 logging.error(msg)
250 raise Exception(msg)
252 response_message = response.choices[0].message
253 if response_message.parsed:
254 return response_message.parsed
255 raise ValueError("LLM response did not contain a parsed Script object.")
257 @retry(stop=stop_after_attempt(3), wait=wait_random_exponential(min=1, max=3))
258 async def gen_script_stream(
259 self,
260 pdf_images: List[str],
261 pdf_text: List[str],
262 max_tokens: int = DEFAULT_MAX_TOKENS,
263 max_dialogues: int = DEFAULT_MAX_DIALOGUES,
264 max_words_per_dialogue: int = -1,
265 num_characters: int = DEFAULT_NUM_CHARACTERS,
266 style_prompt: Optional[str] = None,
267 scene_prompt: Optional[str] = None,
268 custom_prompt: Optional[str] = None,
269 temperature: float = DEFAULT_TEMPERATURE,
270 job_id: Optional[str] = None,
271 ) -> AsyncGenerator[Dict, None]:
272 QUESTION = "What is the main theme of this paper?"
273 system_prompt_text = SYSTEM_PROMPT + f"\n\n{QUESTION_MODIFIER} {QUESTION}"
274 user_prompt_text = "\n\n".join(pdf_text) + f"\n\n{QUESTION_MODIFIER} {QUESTION}"
276 contraint_instruction = ""
277 if max_words_per_dialogue > 0:
278 contraint_instruction += f"Each dialogue should not exceed {max_words_per_dialogue} words.\n"
280 if max_dialogues < 0:
281 logging.warning(f"'max_dialogues' not set, using default value of {DEFAULT_MAX_DIALOGUES}.")
282 max_dialogues = DEFAULT_MAX_DIALOGUES
283 if max_dialogues > 0:
284 contraint_instruction += f"Generate as close as possible to {max_dialogues} dialogues.\n"
285 contraint_instruction += f"Do not generate more than {max_dialogues} dialogues.\n"
286 if max_dialogues > 2:
287 contraint_instruction += "Make the last two dialogues conclude the discussion.\n"
289 if num_characters == 1:
290 contraint_instruction += "Generate an image description with 1 character. "
291 contraint_instruction += "The image description must feature exactly 1 person. "
292 contraint_instruction += "GENERATE ONLY 1 CHARACTER. "
293 contraint_instruction += "The scene must feature a single person. "
294 contraint_instruction += "All dialogues belong to this person. "
295 elif num_characters > 1:
296 contraint_instruction += f"Generate an image description with {num_characters} characters."
297 contraint_instruction += f"The image description must feature exactly {num_characters} people. "
298 contraint_instruction += f"GENERATE ONLY {num_characters} CHARACTERS. "
299 contraint_instruction += f"The scene must feature {num_characters} people. "
300 contraint_instruction += f"Make the {num_characters} characters have a dialogue. "
301 contraint_instruction += f"There must be {num_characters} characters in the dialogues. "
302 contraint_instruction += "Do NOT add any other characters.\n"
304 if style_prompt and style_prompt in IMG_STYLE_MODIFIERS:
305 style_modifier = IMG_STYLE_MODIFIERS[style_prompt]
306 contraint_instruction += f"The style of the image should be {style_modifier}.\n"
307 if scene_prompt and scene_prompt in IMG_SCENE_MODIFIERS:
308 scene_modifier = IMG_SCENE_MODIFIERS[scene_prompt]
309 contraint_instruction += f"The scene of the image should be in {scene_modifier}.\n"
310 if custom_prompt:
311 contraint_instruction += f"The image should have {custom_prompt}.\n"
313 # Compose prompt
314 system_prompt_text = (
315 f"{system_prompt_text}\n\n"
316 f"{contraint_instruction}"
317 )
318 user_prompt_text = (
319 f"{user_prompt_text}\n\n"
320 f"{contraint_instruction}"
321 )
323 # Build the messages
324 user_msg: List[Dict[str, Any]] = [{
325 "type": "text",
326 "text": user_prompt_text
327 }]
328 if self.multi_modal:
329 for image_url in pdf_images:
330 user_msg.append({
331 "type": "image_url",
332 "image_url": {"url": image_url}})
334 messages: List[Dict[str, Any]] = [
335 {"role": "system", "content": system_prompt_text},
336 {"role": "user", "content": user_msg}
337 ]
339 async with aiofiles.open(f"/tmp/{job_id}_prompt.json", "w") as prompt_file:
340 await prompt_file.write(json.dumps(messages, indent=2))
342 response_stream = cast("AsyncStream[ChatCompletionChunk]", await self.llm_client.chat.completions.create(
343 model=self.llm_model,
344 messages=messages, # type: ignore[arg-type]
345 temperature=temperature,
346 max_tokens=max_tokens,
347 extra_body=self.extra_body,
348 stream=True,
349 ))
351 it = 0
352 buffer_text = ""
353 async for chunk in response_stream:
354 if self.is_interrupted():
355 logging.info("Generation interrupted.")
356 return
358 delta = chunk.choices[0].delta.content or ""
359 buffer_text += delta
360 if delta.endswith("\n"):
361 buffer_text = buffer_text.strip()
362 if buffer_text.startswith("{") and buffer_text.endswith("}"):
363 try:
364 buffer_text = fix_json_like_string(buffer_text)
365 buffer_json = json.loads(buffer_text)
366 yield buffer_json
367 except json.JSONDecodeError as json_error:
368 logging.error(f"JSON error: {json_error} for buffer: {buffer_text}")
369 else:
370 logging.info(f"Ignoring: {buffer_text}")
371 buffer_text = ""
372 it += 1
374 async def gen_podcast(
375 self,
376 script: str,
377 max_tokens: int = DEFAULT_MAX_TOKENS,
378 temperature: float = DEFAULT_TEMPERATURE,
379 job_id: Optional[str] = None,
380 ) -> Podcast:
381 # Prepare the user prompt text string
382 user_prompt = "Given the podcast description, split the dialogues into scenes such that some scenes focus on "
383 user_prompt += "one character, others on multiple characters."
384 user_prompt += f"\n\nThe transcript is as follows: {script}"
386 # Prepare the system prompt text string
387 system_prompt = "You are a world-class podcast director who can organize a script into a collection of scenes "
388 system_prompt += "that will be later converted into a video."
390 # Combine the prompts into a single message
391 messages: List[Dict[str, Any]] = [
392 {"role": "system", "content": system_prompt},
393 {"role": "user", "content": user_prompt}
394 ]
396 async with aiofiles.open(f"/tmp/{job_id}_prompt.json", "w") as prompt_file:
397 await prompt_file.write(json.dumps(messages, indent=2))
399 try:
400 response = await self.llm_client.beta.chat.completions.parse(
401 model=self.llm_model,
402 messages=messages, # type: ignore[arg-type]
403 temperature=temperature,
404 max_tokens=max_tokens,
405 extra_body=self.extra_body,
406 response_format=Podcast,
407 )
408 except Exception as ex:
409 msg = f"Cannot query LLM for podcast at {self.llm_url}: {ex}."
410 logging.error(msg)
411 raise Exception(msg)
413 response_message = response.choices[0].message
414 if response_message.parsed:
415 return response_message.parsed
416 raise ValueError("LLM response did not contain a parsed Podcast object.")
418 @override
419 async def generate(
420 self,
421 pdf_url: Optional[str] = None,
422 pdf_text: Optional[List[str]] = None,
423 pdf_images: Optional[List[str]] = None,
424 max_tokens: int = DEFAULT_MAX_TOKENS,
425 temperature: float = DEFAULT_TEMPERATURE,
426 llm_model: Optional[str] = None,
427 llm_url: Optional[str] = None,
428 multi_modal: bool = False,
429 max_dialogues: int = -1,
430 max_words_per_dialogue: int = -1,
431 job_id: Optional[str] = None,
432 ) -> Podcast:
433 gen_timer = self._new_gen_timer(job_id)
435 self.running = True # We can run in parallel but good to know if we are running
437 if llm_model is not None and llm_url is not None:
438 logging.info(f"Setting LLM model to {llm_model} at {llm_url}.")
439 self.set_llm(
440 llm_model,
441 llm_url,
442 multi_modal)
444 try:
445 if pdf_url:
446 if not pdf_url.startswith("http"):
447 raise ValueError(f"PDF URL must start with 'http': {pdf_url}")
448 gen_timer.start("download_pdf")
449 pdf_path = self.download_pdf(pdf_url, job_id)
450 pdf_text, pdf_images = parse_pdf(pdf_path)
451 gen_timer.end("download_pdf")
453 if pdf_text is None or pdf_images is None:
454 raise ValueError("PDF text or images are not provided or could not be parsed.")
456 gen_timer.start("gen_script")
457 script = await self.gen_script(
458 pdf_images,
459 pdf_text,
460 max_tokens=max_tokens,
461 temperature=temperature,
462 max_dialogues=max_dialogues,
463 max_words_per_dialogue=max_words_per_dialogue,
464 job_id=job_id,
465 )
466 gen_timer.end("gen_script")
468 gen_timer.start("gen_podcast")
469 podcast = await self.gen_podcast(
470 str(script),
471 max_tokens=max_tokens,
472 temperature=temperature)
473 gen_timer.end("gen_podcast")
474 finally:
475 self.running = False
476 gen_timer.end("total")
478 return podcast
480 async def generate_stream(
481 self,
482 pdf_url: Optional[str] = None,
483 pdf_text: Optional[List[str]] = None,
484 pdf_images: Optional[List[str]] = None,
485 max_tokens: int = DEFAULT_MAX_TOKENS,
486 temperature: float = DEFAULT_TEMPERATURE,
487 llm_model: Optional[str] = None,
488 llm_url: Optional[str] = None,
489 multi_modal: bool = False,
490 max_dialogues: int = -1,
491 max_words_per_dialogue: int = -1,
492 num_characters: int = DEFAULT_NUM_CHARACTERS,
493 style_prompt: Optional[str] = None,
494 scene_prompt: Optional[str] = None,
495 custom_prompt: Optional[str] = None,
496 job_id: Optional[str] = None,
497 ) -> AsyncGenerator[Dict, None]:
498 gen_timer = self._new_gen_timer(job_id)
500 self.running = True # We can run in parallel but good to know if we are running
502 if llm_model is not None and llm_url is not None:
503 logging.info(f"Setting LLM model to {llm_model} at {llm_url}.")
504 self.set_llm(
505 llm_model,
506 llm_url,
507 multi_modal)
509 try:
510 if pdf_url:
511 if not pdf_url.startswith("http"):
512 raise ValueError(f"PDF URL must start with 'http': {pdf_url}")
513 gen_timer.start("download_pdf")
514 pdf_path = self.download_pdf(pdf_url)
515 pdf_text, pdf_images = parse_pdf(pdf_path)
516 gen_timer.end("download_pdf")
518 if pdf_text is None or pdf_images is None:
519 raise ValueError("PDF text or images are not provided or could not be parsed.")
521 it = 0
522 gen_timer.start("gen_script_stream")
523 gen_timer.start(f"gen_script_stream_{it}")
524 async for dialogue in self.gen_script_stream(
525 pdf_images,
526 pdf_text,
527 max_tokens=max_tokens,
528 temperature=temperature,
529 max_dialogues=max_dialogues,
530 max_words_per_dialogue=max_words_per_dialogue,
531 num_characters=num_characters,
532 style_prompt=style_prompt,
533 scene_prompt=scene_prompt,
534 custom_prompt=custom_prompt,
535 job_id=job_id,
536 ):
537 yield dialogue
538 gen_timer.end(f"gen_script_stream_{it}")
539 it += 1
540 gen_timer.start(f"gen_script_stream_{it}")
541 gen_timer.end(f"gen_script_stream_{it}")
542 gen_timer.end("gen_script_stream")
543 finally:
544 self.running = False
545 gen_timer.end("total")
547 async def warmup(self) -> None:
548 pass # No specific warmup needed for this model
550 async def get_rest_args(
551 self,
552 data_json: Dict[str, str]
553 ) -> Dict[str, Any]:
554 if data_json is None:
555 raise ValueError("Missing JSON body")
557 job_id = data_json.get("job_id", None)
559 pdf_url = data_json.get("pdf_url", None)
561 pdf_base64 = data_json.get("doc", None)
562 pdf_text = None
563 pdf_images = None
564 if pdf_base64 is not None:
565 if not job_id:
566 output_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name
567 else:
568 output_path = f"/tmp/{job_id}.pdf"
569 doc_bytes = base64_to_binary(pdf_base64)
570 async with aiofiles.open(output_path, "wb") as doc_file:
571 await doc_file.write(doc_bytes)
572 pdf_text, pdf_images = parse_pdf(output_path)
574 if pdf_url is None and pdf_text is None and pdf_images is None:
575 raise ValueError("Either 'pdf_url' or 'doc' must be provided")
577 rest_args: Dict[str, Any] = {
578 "job_id": job_id,
579 "temperature": float(data_json.get("temperature", 0.6)),
580 "max_tokens": int(data_json.get("max_tokens", DEFAULT_MAX_TOKENS)),
581 "llm_url": data_json.get("llm_url", "http://localhost:8000/v1"),
582 "llm_model": data_json.get("llm_model", "google/gemma-3-27b-it"),
583 "multi_modal": bool(data_json.get("multi_modal", False)),
584 "max_dialogues": int(data_json.get("max_dialogues", DEFAULT_MAX_DIALOGUES)),
585 "max_words_per_dialogue": int(data_json.get("max_words_per_dialogue", -1)),
586 "num_characters": int(data_json.get("num_characters", DEFAULT_NUM_CHARACTERS)),
587 }
589 if pdf_url is not None:
590 rest_args["pdf_url"] = pdf_url
591 if pdf_text is not None:
592 rest_args["pdf_text"] = pdf_text
593 if pdf_images is not None:
594 rest_args["pdf_images"] = pdf_images
596 if "style_prompt" in data_json:
597 rest_args["style_prompt"] = data_json["style_prompt"]
598 if "scene_prompt" in data_json:
599 rest_args["scene_prompt"] = data_json["scene_prompt"]
600 if "custom_prompt" in data_json:
601 rest_args["custom_prompt"] = data_json["custom_prompt"]
603 return {
604 "task": self.model_name,
605 "args": rest_args
606 }
608 def get_health(self) -> Dict[str, Any]:
609 ret = super().get_health()
610 ret.update({
611 # "gpu": torch.cuda.get_device_name(0)
612 "gpu": None,
613 "llm_url": self.llm_url,
614 "llm_model": self.llm_model,
615 "multi_modal": self.multi_modal,
616 })
617 return ret
620def log_podcast(scene: Dict[str, Any]) -> None:
621 if isinstance(scene, dict):
622 assert "type" in scene
623 line_type = scene["type"]
624 if line_type == "image" and "content" in scene:
625 scene_img_description = scene['content']
626 logging.info(f"IMAGE: {scene_img_description}")
627 elif line_type == "character":
628 gender = scene.get('gender', 'Unknown')
629 description = scene.get('description', 'No description provided')
630 logging.info(
631 f"Character: {scene['name']} ({gender}) - {description}")
632 elif line_type == "dialogue" and "character" in scene and "content" in scene:
633 logging.info(f"[{scene['character']}] {scene['content']}")
634 else:
635 logging.info(f"Scene: {scene}")
636 else:
637 logging.info(f"Scene: {scene}.")
640async def main() -> None:
641 LLM_MODEL = "meta-llama/Meta-Llama-3.1-8B"
642 LMM_MULTI_MODAL = False
644 LLM_MODEL = "google/gemma-3-27b-it"
645 LMM_MULTI_MODAL = True
647 LLM_URL = "http://localhost:8000/v1"
648 LLM_URL = "http://localhost:18086/v1"
650 # Azure OpenAI
651 LLM_MODEL = "gpt-4o-2024-08-06"
652 LLM_URL = "https://girfan-az-openai-001.openai.azure.com/openai/" \
653 "deployments/gpt-4o-2/chat/completions?api-version=2025-01-01-preview"
655 # Gemma on Kubernetes
656 LLM_MODEL = "google/gemma-3-27b-it"
657 LMM_MULTI_MODAL = True
658 LLM_URL = "http://10.244.22.5:8000/v1"
660 logging.basicConfig(level=logging.INFO)
662 logging.info("Starting Podcast Transcript Generation...")
663 logging.info(f"Using LLM model: {LLM_MODEL}.")
664 logging.info(f"Using LLM URL: {LLM_URL}.")
665 logging.info(f"Multi-modal support: {LMM_MULTI_MODAL}.")
667 try:
668 podcast_generator = PodcastTranscriptGenerator(
669 llm_url=LLM_URL,
670 llm_model=LLM_MODEL,
671 multi_modal=LMM_MULTI_MODAL,
672 )
674 PDF_URL = "https://arxiv.org/pdf/2501.16634"
676 # Output podcast transcript using the async generator
677 # Podcast 1
678 logging.info("Generating podcast 1 with a max of 7 dialogues:")
679 async for scene in podcast_generator.generate_stream(
680 pdf_url=PDF_URL,
681 max_dialogues=7,
682 max_words_per_dialogue=20,
683 ):
684 log_podcast(scene)
686 # Podcast 2
687 logging.info("Generating podcast 2 with a max of 20 dialogues:")
688 async for scene in podcast_generator.generate_stream(
689 pdf_url=PDF_URL,
690 max_dialogues=20,
691 max_words_per_dialogue=50,
692 ):
693 log_podcast(scene)
695 # Podcast 3
696 logging.info("Generating podcast 3 with 3 characters and a max of 15 dialogues:")
697 async for scene in podcast_generator.generate_stream(
698 pdf_url=PDF_URL,
699 max_dialogues=15,
700 num_characters=3,
701 max_words_per_dialogue=10,
702 ):
703 log_podcast(scene)
705 # Output podcast transcript in a single pass
706 podcast = await podcast_generator.generate(PDF_URL)
707 for podcast_scene in podcast.scenes:
708 logging.info(f"Scene with characters: {podcast_scene.characters}.")
709 for dialogue in podcast_scene.dialogues:
710 logging.info(f"{dialogue.character}: {dialogue.transcript}.")
711 # We can also output the podcast in JSON
712 podcast_json = podcast.model_dump_json(indent=2)
713 logging.info(f"Podcast JSON:\n{podcast_json}")
715 except Exception as ex:
716 exc_str = ''.join(traceback.format_tb(ex.__traceback__))
717 logging.error(f"An error occurred: {ex}:\n{exc_str}.")
720if __name__ == "__main__":
721 asyncio.run(main())