slide-stream 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """Slide Stream - AI-powered tool for creating video presentations from Markdown."""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Your Name"
5
+ __email__ = "your.email@example.com"
slide_stream/cli.py ADDED
@@ -0,0 +1,242 @@
1
+ """Command line interface for Slide Stream."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from moviepy import ImageClip, concatenate_videoclips
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
12
+
13
+ from . import __version__
14
+ from .config import TEMP_DIR
15
+ from .llm import get_llm_client, query_llm
16
+ from .media import (
17
+ create_text_image,
18
+ create_video_fragment,
19
+ search_and_download_image,
20
+ text_to_speech,
21
+ )
22
+ from .parser import parse_markdown
23
+
24
+ # Rich Console Initialization
25
+ console = Console()
26
+ err_console = Console(stderr=True, style="bold red")
27
+
28
+ # Typer Application Initialization
29
+ app = typer.Typer(
30
+ name="slide-stream",
31
+ help="""
32
+ SlideStream: An AI-powered tool to automatically create video presentations from text and Markdown.
33
+ """,
34
+ add_completion=False,
35
+ rich_markup_mode="markdown",
36
+ )
37
+
38
+
39
+ def version_callback(value: bool) -> None:
40
+ """Print the version of the application and exit."""
41
+ if value:
42
+ console.print(
43
+ f"[bold cyan]SlideStream[/bold cyan] version: [yellow]{__version__}[/yellow]"
44
+ )
45
+ raise typer.Exit()
46
+
47
+
48
+ @app.command()
49
+ def create(
50
+ input_file: Annotated[
51
+ typer.FileText,
52
+ typer.Option(
53
+ "--input",
54
+ "-i",
55
+ help="Path to the input Markdown file.",
56
+ rich_help_panel="Input/Output Options",
57
+ ),
58
+ ],
59
+ output_filename: Annotated[
60
+ str,
61
+ typer.Option(
62
+ "--output",
63
+ "-o",
64
+ help="Filename for the output video.",
65
+ rich_help_panel="Input/Output Options",
66
+ ),
67
+ ] = "output_video.mp4",
68
+ llm_provider: Annotated[
69
+ str,
70
+ typer.Option(
71
+ help="Select the LLM provider for text enhancement.",
72
+ rich_help_panel="AI & Content Options",
73
+ ),
74
+ ] = "none",
75
+ image_source: Annotated[
76
+ str,
77
+ typer.Option(
78
+ help="Choose the source for slide images.",
79
+ rich_help_panel="AI & Content Options",
80
+ ),
81
+ ] = "unsplash",
82
+ version: Annotated[
83
+ bool,
84
+ typer.Option(
85
+ "--version",
86
+ help="Show application version and exit.",
87
+ callback=version_callback,
88
+ is_eager=True,
89
+ ),
90
+ ] = False,
91
+ ) -> None:
92
+ """Create a video from a Markdown file."""
93
+ console.print(
94
+ Panel.fit(
95
+ "[bold cyan]🚀 Starting SlideStream! 🚀[/bold cyan]",
96
+ border_style="green",
97
+ )
98
+ )
99
+
100
+ # Read input file
101
+ markdown_input = input_file.read()
102
+ if not markdown_input.strip():
103
+ err_console.print("Input file is empty. Exiting.")
104
+ raise typer.Exit(code=1)
105
+
106
+ # Setup temporary directory
107
+ temp_dir = Path(TEMP_DIR)
108
+ temp_dir.mkdir(exist_ok=True)
109
+
110
+ # Initialize LLM client
111
+ llm_client = None
112
+ if llm_provider != "none":
113
+ try:
114
+ llm_client = get_llm_client(llm_provider)
115
+ console.print(
116
+ f"✅ LLM Provider Initialized: [bold green]{llm_provider}[/bold green]"
117
+ )
118
+ except (ImportError, ValueError) as e:
119
+ err_console.print(f"Error initializing LLM: {e}")
120
+ raise typer.Exit(code=1)
121
+
122
+ # Parse the Markdown
123
+ console.print("\n[bold]1. Parsing Markdown...[/bold]")
124
+ slides = parse_markdown(markdown_input)
125
+ if not slides:
126
+ err_console.print("No slides found in the Markdown file. Exiting.")
127
+ raise typer.Exit(code=1)
128
+ console.print(f"📄 Found [bold yellow]{len(slides)}[/bold yellow] slides.")
129
+
130
+ # Process each slide with Rich progress bar
131
+ video_fragments = []
132
+ with Progress(
133
+ SpinnerColumn(),
134
+ TextColumn("[progress.description]{task.description}"),
135
+ BarColumn(),
136
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
137
+ transient=True,
138
+ ) as progress:
139
+ process_task = progress.add_task(
140
+ "[yellow]Processing Slides...", total=len(slides)
141
+ )
142
+
143
+ for i, slide in enumerate(slides):
144
+ slide_num = i + 1
145
+ progress.update(
146
+ process_task,
147
+ description=f"[yellow]Processing Slide {slide_num}/{len(slides)}: '{slide['title']}'[/yellow]",
148
+ )
149
+
150
+ raw_text = f"Title: {slide['title']}. Content: {' '.join(slide['content'])}"
151
+ speech_text = raw_text
152
+ search_query = slide["title"]
153
+
154
+ # LLM Processing
155
+ if llm_client:
156
+ speech_prompt = f"Convert the following slide points into a natural, flowing script for a voiceover. Speak conversationally. Directly output the script and nothing else.\n\n{raw_text}"
157
+ natural_speech = query_llm(
158
+ llm_client, llm_provider, speech_prompt, console
159
+ )
160
+ if natural_speech:
161
+ speech_text = natural_speech
162
+
163
+ if image_source == "unsplash":
164
+ search_prompt = f"Generate a concise, descriptive search query for a stock photo website (like Unsplash) to find a high-quality, relevant image for this topic. Output only the query. Topic:\n\n{raw_text}"
165
+ improved_query = query_llm(
166
+ llm_client, llm_provider, search_prompt, console
167
+ )
168
+ if improved_query:
169
+ search_query = improved_query.strip().replace('"', "")
170
+
171
+ # File paths
172
+ img_path = temp_dir / f"slide_{slide_num}.png"
173
+ audio_path = temp_dir / f"slide_{slide_num}.mp3"
174
+ fragment_path = temp_dir / f"fragment_{slide_num}.mp4"
175
+
176
+ # Image sourcing, audio, and video creation
177
+ if image_source == "unsplash":
178
+ search_and_download_image(search_query, str(img_path))
179
+ else:
180
+ create_text_image(slide["title"], slide["content"], str(img_path))
181
+
182
+ audio_file = text_to_speech(speech_text, str(audio_path))
183
+ fragment_file = create_video_fragment(
184
+ str(img_path),
185
+ str(audio_path) if audio_file else None,
186
+ str(fragment_path),
187
+ )
188
+
189
+ if fragment_file:
190
+ video_fragments.append(fragment_file)
191
+
192
+ progress.update(process_task, advance=1)
193
+ time.sleep(0.1) # Small delay for smoother progress bar updates
194
+
195
+ # Combine video fragments
196
+ console.print("\n[bold]2. Combining Video Fragments...[/bold]")
197
+ if video_fragments:
198
+ try:
199
+ clips = [
200
+ ImageClip(f).set_duration(ImageClip(f).duration)
201
+ for f in video_fragments
202
+ ]
203
+ final_clip = concatenate_videoclips(clips)
204
+ final_clip.write_videofile(
205
+ output_filename, fps=24, codec="libx264", audio_codec="aac", logger=None
206
+ )
207
+
208
+ # Clean up clips
209
+ for clip in clips:
210
+ clip.close()
211
+ final_clip.close()
212
+
213
+ console.print(
214
+ Panel(
215
+ f"🎉 [bold green]Video creation complete![/bold green] 🎉\n\nOutput file: [yellow]{output_filename}[/yellow]",
216
+ border_style="green",
217
+ expand=False,
218
+ )
219
+ )
220
+ except Exception as e:
221
+ err_console.print(f"Error combining video fragments: {e}")
222
+ raise typer.Exit(code=1)
223
+ else:
224
+ err_console.print(
225
+ "No video fragments were created, so the final video could not be generated."
226
+ )
227
+ raise typer.Exit(code=1)
228
+
229
+ # Cleanup
230
+ console.print("\n[bold]3. Cleaning up temporary files...[/bold]")
231
+ try:
232
+ for file_path in temp_dir.iterdir():
233
+ if file_path.is_file():
234
+ file_path.unlink()
235
+ temp_dir.rmdir()
236
+ console.print("✅ Cleanup complete.")
237
+ except Exception as e:
238
+ err_console.print(f"Warning: Could not clean up temporary files: {e}")
239
+
240
+
241
+ if __name__ == "__main__":
242
+ app()
slide_stream/config.py ADDED
@@ -0,0 +1,25 @@
1
+ """Configuration constants for Slide Stream."""
2
+
3
+
4
+ # Video settings
5
+ VIDEO_RESOLUTION = (1920, 1080)
6
+ SLIDE_DURATION_PADDING = 1.0
7
+ DEFAULT_SLIDE_DURATION = 5.0
8
+ VIDEO_FPS = 24
9
+ VIDEO_CODEC = "libx264"
10
+ AUDIO_CODEC = "aac"
11
+
12
+ # Image settings
13
+ IMAGE_DOWNLOAD_TIMEOUT = 15
14
+ BG_COLOR = "black"
15
+
16
+ # Directories
17
+ TEMP_DIR = "temp_files"
18
+
19
+ # Font settings
20
+ DEFAULT_TITLE_FONT_SIZE = 100
21
+ DEFAULT_CONTENT_FONT_SIZE = 60
22
+ FONT_COLOR = "white"
23
+
24
+ # Text wrapping
25
+ MAX_LINE_WIDTH = 50
slide_stream/llm.py ADDED
@@ -0,0 +1,118 @@
1
+ """LLM integration for Slide Stream."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ from rich.console import Console
7
+
8
+ err_console = Console(stderr=True, style="bold red")
9
+
10
+
11
+ def get_llm_client(provider: str) -> Any:
12
+ """Get LLM client based on provider."""
13
+ if provider == "gemini":
14
+ try:
15
+ import google.generativeai as genai
16
+
17
+ api_key = os.getenv("GEMINI_API_KEY")
18
+ if not api_key:
19
+ raise ValueError("GEMINI_API_KEY environment variable not set.")
20
+ genai.configure(api_key=api_key)
21
+ return genai.GenerativeModel("gemini-1.5-flash")
22
+ except ImportError:
23
+ raise ImportError(
24
+ "Gemini library not found. Please install with: pip install slide-stream[gemini]"
25
+ )
26
+
27
+ elif provider == "openai":
28
+ try:
29
+ from openai import OpenAI
30
+
31
+ api_key = os.getenv("OPENAI_API_KEY")
32
+ if not api_key:
33
+ raise ValueError("OPENAI_API_KEY environment variable not set.")
34
+ return OpenAI(api_key=api_key)
35
+ except ImportError:
36
+ raise ImportError(
37
+ "OpenAI library not found. Please install with: pip install slide-stream[openai]"
38
+ )
39
+
40
+ elif provider == "claude":
41
+ try:
42
+ import anthropic
43
+
44
+ api_key = os.getenv("ANTHROPIC_API_KEY")
45
+ if not api_key:
46
+ raise ValueError("ANTHROPIC_API_KEY environment variable not set.")
47
+ return anthropic.Anthropic(api_key=api_key)
48
+ except ImportError:
49
+ raise ImportError(
50
+ "Anthropic library not found. Please install with: pip install slide-stream[claude]"
51
+ )
52
+
53
+ elif provider == "groq":
54
+ try:
55
+ from groq import Groq
56
+
57
+ api_key = os.getenv("GROQ_API_KEY")
58
+ if not api_key:
59
+ raise ValueError("GROQ_API_KEY environment variable not set.")
60
+ return Groq(api_key=api_key)
61
+ except ImportError:
62
+ raise ImportError(
63
+ "Groq library not found. Please install with: pip install slide-stream[groq]"
64
+ )
65
+
66
+ elif provider == "ollama":
67
+ try:
68
+ from openai import OpenAI
69
+
70
+ base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
71
+ return OpenAI(base_url=f"{base_url}/v1", api_key="ollama")
72
+ except ImportError:
73
+ raise ImportError(
74
+ "OpenAI library not found. Please install with: pip install slide-stream[openai]"
75
+ )
76
+
77
+ else:
78
+ raise ValueError(f"Unknown LLM provider: {provider}")
79
+
80
+
81
+ def query_llm(
82
+ client: Any, provider: str, prompt_text: str, rich_console: Console
83
+ ) -> str | None:
84
+ """Query LLM with given prompt."""
85
+ rich_console.print(" - Querying LLM...")
86
+
87
+ try:
88
+ if provider == "gemini":
89
+ response = client.generate_content(prompt_text)
90
+ return response.text
91
+
92
+ elif provider in ["openai", "ollama"]:
93
+ model = "gpt-4o" if provider == "openai" else "llama3.1"
94
+ response = client.chat.completions.create(
95
+ model=model, messages=[{"role": "user", "content": prompt_text}]
96
+ )
97
+ return response.choices[0].message.content
98
+
99
+ elif provider == "claude":
100
+ response = client.messages.create(
101
+ model="claude-3-sonnet-20240229",
102
+ max_tokens=1024,
103
+ messages=[{"role": "user", "content": prompt_text}],
104
+ )
105
+ return response.content[0].text
106
+
107
+ elif provider == "groq":
108
+ response = client.chat.completions.create(
109
+ model="llama3-8b-8192",
110
+ messages=[{"role": "user", "content": prompt_text}],
111
+ )
112
+ return response.choices[0].message.content
113
+
114
+ return None
115
+
116
+ except Exception as e:
117
+ err_console.print(f" - LLM Error: {e}")
118
+ return None
slide_stream/media.py ADDED
@@ -0,0 +1,149 @@
1
+ """Media handling functionality for Slide Stream."""
2
+
3
+ import os
4
+ import textwrap
5
+
6
+ import requests
7
+ from gtts import gTTS
8
+ from moviepy import AudioFileClip, ImageClip
9
+ from PIL import Image, ImageDraw, ImageFont
10
+ from rich.console import Console
11
+
12
+ from .config import (
13
+ BG_COLOR,
14
+ DEFAULT_CONTENT_FONT_SIZE,
15
+ DEFAULT_SLIDE_DURATION,
16
+ DEFAULT_TITLE_FONT_SIZE,
17
+ FONT_COLOR,
18
+ IMAGE_DOWNLOAD_TIMEOUT,
19
+ MAX_LINE_WIDTH,
20
+ SLIDE_DURATION_PADDING,
21
+ VIDEO_CODEC,
22
+ VIDEO_FPS,
23
+ VIDEO_RESOLUTION,
24
+ )
25
+
26
+ err_console = Console(stderr=True, style="bold red")
27
+
28
+
29
+ def search_and_download_image(query: str, filename: str) -> str:
30
+ """Download image from Unsplash based on search query."""
31
+ try:
32
+ url = f"https://source.unsplash.com/random/{VIDEO_RESOLUTION[0]}x{VIDEO_RESOLUTION[1]}/?{query.replace(' ', ',')}"
33
+ response = requests.get(
34
+ url, timeout=IMAGE_DOWNLOAD_TIMEOUT, allow_redirects=True
35
+ )
36
+ response.raise_for_status()
37
+
38
+ with open(filename, "wb") as f:
39
+ f.write(response.content)
40
+
41
+ return filename
42
+
43
+ except requests.exceptions.RequestException as e:
44
+ err_console.print(f" - Image download error: {e}. Using a placeholder.")
45
+ return create_text_image("Image not found", [f"Query: {query}"], filename)
46
+
47
+
48
+ def create_text_image(title: str, content_items: list, filename: str) -> str:
49
+ """Create a text-based image for slides."""
50
+ img = Image.new("RGB", VIDEO_RESOLUTION, color=BG_COLOR)
51
+ draw = ImageDraw.Draw(img)
52
+
53
+ # Try to load custom fonts, fall back to default
54
+ try:
55
+ title_font = ImageFont.truetype("arial.ttf", DEFAULT_TITLE_FONT_SIZE)
56
+ content_font = ImageFont.truetype("arial.ttf", DEFAULT_CONTENT_FONT_SIZE)
57
+ except OSError:
58
+ try:
59
+ # Try alternative common font names
60
+ title_font = ImageFont.truetype("DejaVuSans.ttf", DEFAULT_TITLE_FONT_SIZE)
61
+ content_font = ImageFont.truetype(
62
+ "DejaVuSans.ttf", DEFAULT_CONTENT_FONT_SIZE
63
+ )
64
+ except OSError:
65
+ title_font = ImageFont.load_default()
66
+ content_font = ImageFont.load_default()
67
+
68
+ # Draw title
69
+ draw.text(
70
+ (VIDEO_RESOLUTION[0] * 0.1, VIDEO_RESOLUTION[1] * 0.1),
71
+ title,
72
+ font=title_font,
73
+ fill=FONT_COLOR,
74
+ )
75
+
76
+ # Draw content items
77
+ y_pos = VIDEO_RESOLUTION[1] * 0.3
78
+ for item in content_items:
79
+ wrapped_lines = textwrap.wrap(f"• {item}", width=MAX_LINE_WIDTH)
80
+ for line in wrapped_lines:
81
+ draw.text(
82
+ (VIDEO_RESOLUTION[0] * 0.1, y_pos),
83
+ line,
84
+ font=content_font,
85
+ fill=FONT_COLOR,
86
+ )
87
+ y_pos += 70
88
+ y_pos += 30
89
+
90
+ img.save(filename)
91
+ return filename
92
+
93
+
94
+ def text_to_speech(text: str, filename: str) -> str | None:
95
+ """Convert text to speech using gTTS."""
96
+ try:
97
+ tts = gTTS(text=text, lang="en")
98
+ tts.save(filename)
99
+ return filename
100
+ except Exception as e:
101
+ err_console.print(f" - Audio generation error: {e}")
102
+ return None
103
+
104
+
105
+ def create_video_fragment(
106
+ image_path: str, audio_path: str | None, output_path: str
107
+ ) -> str | None:
108
+ """Create video fragment from image and audio."""
109
+ try:
110
+ # Load audio if it exists
111
+ audio_clip = None
112
+ if audio_path and os.path.exists(audio_path):
113
+ audio_clip = AudioFileClip(audio_path)
114
+
115
+ # Determine duration
116
+ duration = (
117
+ audio_clip.duration + SLIDE_DURATION_PADDING
118
+ if audio_clip
119
+ else DEFAULT_SLIDE_DURATION
120
+ )
121
+
122
+ # Create image clip
123
+ image_clip = ImageClip(image_path, duration=duration).set_position("center")
124
+
125
+ # Resize image to fit resolution if needed
126
+ if image_clip.h > VIDEO_RESOLUTION[1]:
127
+ image_clip = image_clip.resize(height=VIDEO_RESOLUTION[1])
128
+ if image_clip.w > VIDEO_RESOLUTION[0]:
129
+ image_clip = image_clip.resize(width=VIDEO_RESOLUTION[0])
130
+
131
+ # Combine with audio
132
+ final_clip = image_clip.set_audio(audio_clip) if audio_clip else image_clip
133
+
134
+ # Write video file
135
+ final_clip.write_videofile(
136
+ output_path, fps=VIDEO_FPS, codec=VIDEO_CODEC, logger=None
137
+ )
138
+
139
+ # Clean up clips
140
+ if audio_clip:
141
+ audio_clip.close()
142
+ image_clip.close()
143
+ final_clip.close()
144
+
145
+ return output_path
146
+
147
+ except Exception as e:
148
+ err_console.print(f" - Video fragment creation error: {e}")
149
+ return None
slide_stream/parser.py ADDED
@@ -0,0 +1,38 @@
1
+ """Markdown parsing functionality for Slide Stream."""
2
+
3
+ from typing import Any
4
+
5
+ import markdown
6
+ from bs4 import BeautifulSoup
7
+
8
+
9
+ def parse_markdown(markdown_text: str) -> list[dict[str, Any]]:
10
+ """Parse markdown text into slide data."""
11
+ html = markdown.markdown(markdown_text)
12
+ soup = BeautifulSoup(html, "html.parser")
13
+ slides = []
14
+
15
+ for header in soup.find_all("h1"):
16
+ slide_title = header.get_text()
17
+
18
+ # Find the next sibling that is a list (ul or ol)
19
+ next_sibling = header.find_next_sibling()
20
+ content_items = []
21
+
22
+ while next_sibling:
23
+ if next_sibling.name in ["ul", "ol"]:
24
+ content_items = [
25
+ item.get_text() for item in next_sibling.find_all("li")
26
+ ]
27
+ break
28
+ elif next_sibling.name == "p":
29
+ # If it's a paragraph, add it as content
30
+ content_items.append(next_sibling.get_text())
31
+ elif next_sibling.name in ["h1", "h2", "h3"]:
32
+ # Stop if we hit another header
33
+ break
34
+ next_sibling = next_sibling.find_next_sibling()
35
+
36
+ slides.append({"title": slide_title, "content": content_items})
37
+
38
+ return slides
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: slide-stream
3
+ Version: 1.0.0
4
+ Summary: An AI-powered tool to automatically create video presentations from text and Markdown
5
+ Project-URL: Homepage, https://github.com/yourusername/slide-stream
6
+ Project-URL: Repository, https://github.com/yourusername/slide-stream
7
+ Project-URL: Issues, https://github.com/yourusername/slide-stream/issues
8
+ Author-email: Your Name <your.email@example.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,cli,markdown,presentation,video
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Multimedia :: Video
23
+ Classifier: Topic :: Text Processing :: Markup
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: anthropic>=0.7.0; extra == 'claude'
26
+ Requires-Dist: beautifulsoup4>=4.12.0
27
+ Requires-Dist: google-generativeai>=0.3.0; extra == 'gemini'
28
+ Requires-Dist: groq>=0.4.0; extra == 'groq'
29
+ Requires-Dist: gtts>=2.4.0
30
+ Requires-Dist: markdown>=3.5.0
31
+ Requires-Dist: moviepy>=1.0.3
32
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
33
+ Requires-Dist: pillow>=10.0.0
34
+ Requires-Dist: requests>=2.31.0
35
+ Requires-Dist: rich>=13.0.0
36
+ Requires-Dist: typer>=0.9.0
37
+ Requires-Dist: typing-extensions>=4.8.0
38
+ Provides-Extra: all-ai
39
+ Requires-Dist: anthropic>=0.7.0; extra == 'all-ai'
40
+ Requires-Dist: google-generativeai>=0.3.0; extra == 'all-ai'
41
+ Requires-Dist: groq>=0.4.0; extra == 'all-ai'
42
+ Requires-Dist: openai>=1.0.0; extra == 'all-ai'
43
+ Provides-Extra: claude
44
+ Requires-Dist: anthropic>=0.7.0; extra == 'claude'
45
+ Provides-Extra: dev
46
+ Requires-Dist: basedpyright>=1.8.0; extra == 'dev'
47
+ Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
48
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
49
+ Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
50
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
51
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
52
+ Provides-Extra: gemini
53
+ Requires-Dist: google-generativeai>=0.3.0; extra == 'gemini'
54
+ Provides-Extra: groq
55
+ Requires-Dist: groq>=0.4.0; extra == 'groq'
56
+ Provides-Extra: openai
57
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
58
+ Description-Content-Type: text/markdown
59
+
60
+ # Slide Stream
61
+
62
+ 🎬 An AI-powered tool to automatically create video presentations from Markdown files.
63
+
64
+ [![PyPI version](https://badge.fury.io/py/slide-stream.svg)](https://badge.fury.io/py/slide-stream)
65
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
66
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
67
+
68
+ Transform your Markdown documents into professional video presentations with AI-powered content enhancement, automatic image sourcing, and natural text-to-speech narration.
69
+
70
+ ## ✨ Features
71
+
72
+ - 📝 **Markdown to Video**: Convert Markdown files into professional video presentations
73
+ - 🤖 **AI Enhancement**: Improve content using OpenAI, Gemini, Claude, Groq, or Ollama
74
+ - 🖼️ **Smart Images**: Automatic image sourcing from Unsplash or generate text-based slides
75
+ - 🎙️ **Text-to-Speech**: Natural narration using Google Text-to-Speech
76
+ - 🎨 **Customizable**: Professional video output with configurable settings
77
+ - ⚡ **Modern CLI**: Built with Typer and Rich for excellent user experience
78
+
79
+ ## 🚀 Quick Start
80
+
81
+ ### Installation
82
+
83
+ ```bash
84
+ pip install slide-stream
85
+ ```
86
+
87
+ ### Basic Usage
88
+
89
+ Create a simple Markdown file:
90
+
91
+ ```markdown
92
+ # Welcome to My Presentation
93
+
94
+ - This is the first point
95
+ - Here's another important point
96
+ - And a final thought
97
+
98
+ # Second Slide
99
+
100
+ - More content here
101
+ - Additional information
102
+ - Conclusion
103
+ ```
104
+
105
+ Generate your video:
106
+
107
+ ```bash
108
+ slide-stream create -i presentation.md -o my-video.mp4
109
+ ```
110
+
111
+ ## 🔧 Installation Options
112
+
113
+ ### Core Installation
114
+ ```bash
115
+ pip install slide-stream
116
+ ```
117
+
118
+ ### With AI Providers
119
+ ```bash
120
+ # For OpenAI GPT models
121
+ pip install slide-stream[openai]
122
+
123
+ # For Google Gemini
124
+ pip install slide-stream[gemini]
125
+
126
+ # For Anthropic Claude
127
+ pip install slide-stream[claude]
128
+
129
+ # For Groq (fast inference)
130
+ pip install slide-stream[groq]
131
+
132
+ # For all AI providers
133
+ pip install slide-stream[all-ai]
134
+ ```
135
+
136
+ ## 🎯 Usage Examples
137
+
138
+ ### Basic video creation
139
+ ```bash
140
+ slide-stream create -i slides.md -o presentation.mp4
141
+ ```
142
+
143
+ ### With AI enhancement
144
+ ```bash
145
+ # Set your API key
146
+ export OPENAI_API_KEY="your-api-key-here"
147
+
148
+ # Create enhanced video
149
+ slide-stream create \
150
+ -i slides.md \
151
+ -o presentation.mp4 \
152
+ --llm-provider openai \
153
+ --image-source unsplash
154
+ ```
155
+
156
+ ### Text-only slides (no internet required)
157
+ ```bash
158
+ slide-stream create \
159
+ -i slides.md \
160
+ -o presentation.mp4 \
161
+ --image-source text
162
+ ```
163
+
164
+ ## 🔑 Environment Variables
165
+
166
+ Set these environment variables for AI providers:
167
+
168
+ ```bash
169
+ # OpenAI
170
+ export OPENAI_API_KEY="your-openai-key"
171
+
172
+ # Google Gemini
173
+ export GEMINI_API_KEY="your-gemini-key"
174
+
175
+ # Anthropic Claude
176
+ export ANTHROPIC_API_KEY="your-claude-key"
177
+
178
+ # Groq
179
+ export GROQ_API_KEY="your-groq-key"
180
+
181
+ # Ollama (local)
182
+ export OLLAMA_BASE_URL="http://localhost:11434"
183
+ ```
184
+
185
+ ## 📋 Requirements
186
+
187
+ - Python 3.10+
188
+ - FFmpeg (for video processing)
189
+ - Internet connection (for Unsplash images and AI providers)
190
+
191
+ ## 🤝 Contributing
192
+
193
+ Contributions are welcome! Please feel free to submit a Pull Request.
194
+
195
+ ## 📄 License
196
+
197
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
198
+
199
+ ## 🙏 Acknowledgments
200
+
201
+ - Built with [Typer](https://typer.tiangolo.com/) for the CLI
202
+ - [Rich](https://rich.readthedocs.io/) for beautiful terminal output
203
+ - [MoviePy](https://moviepy.readthedocs.io/) for video processing
204
+ - [Pillow](https://pillow.readthedocs.io/) for image handling
@@ -0,0 +1,11 @@
1
+ slide_stream/__init__.py,sha256=YPuLXtn8zbpE3HN3zAjG63REnJzF7z4blOP5r_Tu7c4,170
2
+ slide_stream/cli.py,sha256=9ImnPa6y7xPY6mqZOJ0u_B1FlmfDE6Ea67dsPOyVFUY,8089
3
+ slide_stream/config.py,sha256=AEenMpsNuhMawBvnkH0gN26qT7pYaf5puiU9UWi59Ow,456
4
+ slide_stream/llm.py,sha256=c3_zRM9Nu6T6ixyTLPK31BGSsSfkn4TOyHjTxwugWco,3949
5
+ slide_stream/media.py,sha256=amNkYilDaARgCU260fEELAy3joJJYJvBZat8cN9Cmuw,4576
6
+ slide_stream/parser.py,sha256=_9Mq0Xngg7_u6X-SmVE4xZjYAKUwI9ALWHoPM1cBrKQ,1232
7
+ slide_stream-1.0.0.dist-info/METADATA,sha256=H62mOMRozxGEJX2ZqNSJNexUPbiePDvyHX-iK_2Mqv4,5873
8
+ slide_stream-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ slide_stream-1.0.0.dist-info/entry_points.txt,sha256=g7GgszRmRunGIcmwanS_RX2_I44odc0WGQrLdikuSbc,54
10
+ slide_stream-1.0.0.dist-info/licenses/LICENSE,sha256=g1qHuBwyB3NcBzHvVFp-N1vV6q7Y8iDu1YDY01cp3p8,1081
11
+ slide_stream-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ slide-stream = slide_stream.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Slide Stream Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.