Skip to content

Commit f75e8f8

Browse files
committed
New PodcastStudio class methods and fixed notebook example
1 parent 9f24686 commit f75e8f8

File tree

10 files changed

+436
-916
lines changed

10 files changed

+436
-916
lines changed

examples/01_basics_notebook.ipynb

+63-790
Large diffs are not rendered by default.

pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "neuralnoise"
3-
version = "1.3.2"
3+
version = "1.4.0"
44
description = "An AI-powered podcast studio that uses multiple AI agents working together."
55
authors = [
66
{ name = "Leonardo Piñeyro", email = "[email protected]" }
@@ -58,6 +58,9 @@ local = [
5858
"docker>=7.1.0",
5959
"ollama>=0.3.3",
6060
]
61+
streamlit = [
62+
"streamlit>=1.39.0",
63+
]
6164

6265
[build-system]
6366
requires = ["hatchling"]

src/neuralnoise/__init__.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
from neuralnoise.extract import extract_content, aextract_content
2-
from neuralnoise.studio import create_podcast_episode
1+
from neuralnoise.extract import aextract_content, extract_content
2+
from neuralnoise.studio import PodcastStudio, generate_podcast_episode
33

4-
__all__ = ["create_podcast_episode", "extract_content", "aextract_content"]
4+
__all__ = [
5+
"aextract_content",
6+
"extract_content",
7+
"generate_podcast_episode",
8+
"PodcastStudio",
9+
]

src/neuralnoise/cli.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from tabulate import tabulate
1010

1111
from neuralnoise.extract import extract_content
12-
from neuralnoise.studio import create_podcast_episode
12+
from neuralnoise.studio import generate_podcast_episode
1313
from neuralnoise.utils import package_root
1414

1515
app = typer.Typer()
@@ -65,7 +65,7 @@ def generate(
6565
f.write(content)
6666

6767
typer.secho(f"Generating podcast episode {name}", fg=typer.colors.GREEN)
68-
create_podcast_episode(
68+
generate_podcast_episode(
6969
name,
7070
content,
7171
config_path=config,

src/neuralnoise/studio/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
from neuralnoise.studio.agents import PodcastStudio # noqa
2-
from neuralnoise.studio.create import create_podcast_episode # noqa
2+
from neuralnoise.studio.generate import generate_podcast_episode # noqa

src/neuralnoise/studio/agents.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
import json
23
import os
34
from pathlib import Path
@@ -11,12 +12,16 @@
1112
GroupChatManager,
1213
UserProxyAgent,
1314
)
15+
from pydub import AudioSegment
16+
from pydub.effects import normalize
17+
from tqdm.auto import tqdm
1418

19+
from neuralnoise.models import StudioConfig
1520
from neuralnoise.studio.hooks import (
1621
optimize_chat_history_hook,
1722
save_last_json_message_hook,
1823
)
19-
from neuralnoise.models import StudioConfig
24+
from neuralnoise.tts import generate_audio_segment
2025
from neuralnoise.utils import package_root
2126

2227

@@ -184,3 +189,49 @@ def is_termination_msg(message):
184189
}
185190

186191
return final_script
192+
193+
def generate_podcast_from_script(self, script: dict[str, Any]) -> AudioSegment:
194+
script_segments = []
195+
196+
temp_dir = self.work_dir / "segments"
197+
temp_dir.mkdir(exist_ok=True)
198+
199+
sections_ids = list(sorted(script["sections"].keys()))
200+
script_segments = [
201+
(section_id, segment)
202+
for section_id in sections_ids
203+
for segment in script["sections"][section_id]["segments"]
204+
]
205+
206+
audio_segments = []
207+
208+
for section_id, segment in tqdm(
209+
script_segments,
210+
desc="Generating audio segments",
211+
):
212+
speaker = self.config.speakers[segment["speaker"]]
213+
content = segment["content"]
214+
215+
content = content.replace("¡", "").replace("¿", "")
216+
217+
content_hash = hashlib.md5(content.encode("utf-8")).hexdigest()
218+
segment_path = temp_dir / f"{section_id}_{segment['id']}_{content_hash}.mp3"
219+
220+
audio_segment = generate_audio_segment(
221+
content, speaker, output_path=segment_path
222+
)
223+
224+
audio_segments.append(audio_segment)
225+
226+
if blank_duration := segment.get("blank_duration"):
227+
silence = AudioSegment.silent(duration=blank_duration * 1000)
228+
audio_segments.append(silence)
229+
230+
podcast = AudioSegment.empty()
231+
232+
for chunk in audio_segments:
233+
podcast += chunk
234+
235+
podcast = normalize(podcast)
236+
237+
return podcast

src/neuralnoise/studio/create.py

-116
This file was deleted.

src/neuralnoise/studio/generate.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import json
2+
import logging
3+
from pathlib import Path
4+
from typing import Literal
5+
6+
from pydub import AudioSegment
7+
8+
from neuralnoise.models import StudioConfig
9+
from neuralnoise.studio import PodcastStudio
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def generate_podcast_episode(
15+
name: str,
16+
content: str,
17+
config: StudioConfig | None = None,
18+
config_path: str | Path | None = None,
19+
format: Literal["wav", "mp3", "ogg"] = "wav",
20+
only_script: bool = False,
21+
) -> AudioSegment | None:
22+
"""Generate a podcast episode from a given content.
23+
24+
Args:
25+
name: Name of the podcast episode.
26+
content: Content to generate the podcast episode from.
27+
config: Studio configuration (optional).
28+
config_path: Path to the studio configuration file (optional).
29+
format: Format of the podcast episode.
30+
only_script: Whether to only generate the script and not the podcast.
31+
"""
32+
# Create output directory
33+
output_dir = Path("output") / name
34+
output_dir.mkdir(parents=True, exist_ok=True)
35+
36+
# Load configuration
37+
if config_path:
38+
logger.info("🔧 Loading configuration from %s", config_path)
39+
with open(config_path, "r") as f:
40+
config = StudioConfig.model_validate_json(f.read())
41+
42+
if not config:
43+
raise ValueError("No studio configuration provided")
44+
45+
studio = PodcastStudio(work_dir=output_dir, config=config)
46+
47+
# Generate the script
48+
script_path = output_dir / "script.json"
49+
50+
if script_path.exists():
51+
logger.info("💬 Loading cached script")
52+
script = json.loads(script_path.read_text())
53+
else:
54+
logger.info("💬 Generating podcast script")
55+
script = studio.generate_script(content)
56+
57+
script_path.write_text(json.dumps(script, ensure_ascii=False))
58+
59+
if only_script:
60+
return None
61+
62+
# Generate audio segments and create the podcast
63+
logger.info("🎙️ Recording podcast episode")
64+
podcast = studio.generate_podcast_from_script(script)
65+
66+
# Export podcast
67+
podcast_filepath = output_dir / f"output.{format}"
68+
logger.info("️💾 Exporting podcast to %s", podcast_filepath)
69+
podcast.export(podcast_filepath, format=format)
70+
71+
logger.info("✅ Podcast generation complete")
72+
73+
return podcast

src/neuralnoise/tts.py

-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ def generate_audio_segment(
7676
overwrite: bool = False,
7777
) -> AudioSegment:
7878
if not output_path.exists() or overwrite:
79-
print(f"Generating {output_path} with content: {content[:80]}...")
8079
tts_function = TTS_PROVIDERS[speaker.settings.provider]
8180
audio = tts_function(content, speaker)
8281

0 commit comments

Comments
 (0)