Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions podcastfy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,19 @@ def process_content(
model_name: Optional[str] = None,
api_key_label: Optional[str] = None,
topic: Optional[str] = None,
longform: bool = False
longform: bool = False,
logger_debug: bool = False,
keep_files: bool = False,
):
"""
Process URLs, a transcript file, image paths, or raw text to generate a podcast or transcript.
"""
try:
if logger_debug:
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
if config is None:
config = load_config()

Expand Down Expand Up @@ -129,6 +136,7 @@ def process_content(
model=tts_model,
api_key=api_key,
conversation_config=conv_config.to_dict(),
keep=keep_files,
)

random_filename = f"podcast_{uuid.uuid4().hex}.mp3"
Expand All @@ -143,6 +151,8 @@ def process_content(
return transcript_filepath

except Exception as e:
import traceback
traceback.print_exc()
logger.error(f"An error occurred in the process_content function: {str(e)}")
raise

Expand Down Expand Up @@ -198,6 +208,18 @@ def main(
"-lf",
help="Generate long-form content (only available for text input without images)"
),
debug: bool = typer.Option(
False,
"--debug",
"-d",
help="Generate debug output"
),
keep: bool = typer.Option(
False,
"--keep-files",
"-k",
help="Keep temporary audio files for further introspection"
),
):
"""
Generate a podcast or transcript from a list of URLs, a file containing URLs, a transcript file, image files, or raw text.
Expand Down Expand Up @@ -231,7 +253,9 @@ def main(
model_name=llm_model_name,
api_key_label=api_key_label,
topic=topic,
longform=longform
longform=longform,
logger_debug=debug,
keep_files=keep,
)
else:
urls_list = urls or []
Expand All @@ -255,7 +279,9 @@ def main(
model_name=llm_model_name,
api_key_label=api_key_label,
topic=topic,
longform=longform
longform=longform,
logger_debug=debug,
keep_files=keep,
)

if transcript_only:
Expand Down
67 changes: 60 additions & 7 deletions podcastfy/text_to_speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import os
import re
import tempfile
import shutil
from typing import List, Tuple, Optional, Dict, Any
from pydub import AudioSegment

Expand All @@ -27,6 +28,7 @@ def __init__(
model: str = None,
api_key: Optional[str] = None,
conversation_config: Optional[Dict[str, Any]] = None,
keep: bool = False
):
"""
Initialize the TextToSpeech class.
Expand Down Expand Up @@ -54,6 +56,7 @@ def __init__(
self._setup_directories()
self.audio_format = self.tts_config.get("audio_format", "mp3")
self.ending_message = self.tts_config.get("ending_message", "")
self.keep = keep

def _get_provider_config(self) -> Dict[str, Any]:
"""Get provider-specific configuration."""
Expand Down Expand Up @@ -123,10 +126,21 @@ def convert_to_speech(self, text: str, output_file: str) -> None:
#with open(temp_file, "wb") as f:
# f.write(chunk)

segment = AudioSegment.from_file(io.BytesIO(chunk))
logger.info(f"################### Loaded chunk {i}, duration: {len(segment)}ms")
try:
segment = AudioSegment.from_file(io.BytesIO(chunk))
if len(segment) < 100 or segment.frame_count() == 0:
logger.error(f"[skip] Chunk {i} is too short or empty")
continue
# Ensure sample_width is valid
if not isinstance(segment.sample_width, int) or segment.sample_width <= 0:
logger.error(f"[skip] Chunk {i} has invalid sample width")
continue
logger.info(f"################### Loaded chunk {i}, duration: {len(segment)}ms")

combined += segment
combined += segment
except Exception as e:
logger.exception(f"[fail] Failed to load/process chunk {i}: {e}")
continue

# Export with high quality settings
os.makedirs(os.path.dirname(output_file), exist_ok=True)
Expand All @@ -141,12 +155,19 @@ def convert_to_speech(self, text: str, output_file: str) -> None:
logger.error(f"Error during audio processing: {str(e)}")
raise
else:
with tempfile.TemporaryDirectory(dir=self.temp_audio_dir) as temp_dir:
temp_dir = tempfile.mkdtemp(dir=self.temp_audio_dir)
try:
logger.debug(f"Using temp dir: {temp_dir}")
audio_segments = self._generate_audio_segments(
cleaned_text, temp_dir
)
self._merge_audio_files(audio_segments, output_file)
logger.info(f"Audio saved to {output_file}")
finally:
if self.keep:
logger.debug(f"Preserved temp audio in: {temp_dir}")
else:
shutil.rmtree(temp_dir)

except Exception as e:
logger.error(f"Error converting text to speech: {str(e)}")
Expand Down Expand Up @@ -202,16 +223,48 @@ def get_sort_key(file_path: str) -> Tuple[int, int]:
# Sort files by index and type (question/answer)
audio_files.sort(key=get_sort_key)

# Create empty audio segment
combined = AudioSegment.empty()
# Create empty audio segment - with a proper valide frame wrate and
# sample width otherwise we can't combine this later.
combined = AudioSegment.silent(duration=0, frame_rate=24000).set_sample_width(2).set_channels(1)

if not audio_files:
logger.warning("No audio files were created. Possible TTS issue.")

# Add each audio file to the combined segment
for file_path in audio_files:
combined += AudioSegment.from_file(file_path, format=self.audio_format)
try:
logger.debug(f"Loading audio file: {file_path}")
audio = AudioSegment.from_file(file_path, format=self.audio_format)
logger.debug(f"Sample width: {audio.sample_width}, Frame rate: {audio.frame_rate}, Channels: {audio.channels}")
if len(audio) < 100: # less than 100 ms
logger.error(f"Skipping too short audio file: {file_path}")
continue
if audio.frame_count() == 0 or len(audio.raw_data) == 0:
logger.error(f"Skipping empty audio file or with no frame count: {file_path}")
continue
if audio._data is None or len(audio._data) == 0:
logger.error(f"Skipping file with no raw audio: {file_path}")
continue
# This prevents pydub from passing a float into audioop,
# which strictly requires an int.
if not isinstance(audio.sample_width, int):
logger.warning(f"Converting sample_width from float to int for: {file_path}")
audio = audio.set_sample_width(int(audio.sample_width))
if audio.sample_width <= 0:
logger.error(f"Skipping file with invalid sample width: {file_path}")
continue
combined += audio
except Exception as e:
logger.error(f"Failed to load or process file: {file_path}: with exception: {e}")
continue

# Ensure output directory exists
os.makedirs(os.path.dirname(output_file), exist_ok=True)

if combined.duration_seconds == 0:
logger.error("No valid audio segments found to merge. Skipping export.")
return

# Export the combined audio
combined.export(output_file, format=self.audio_format)
logger.info(f"Merged audio saved to {output_file}")
Expand Down