quicktools-atom 0.7.1__tar.gz → 0.7.3__tar.gz

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.
Files changed (28) hide show
  1. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/PKG-INFO +2 -2
  2. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/README.md +1 -1
  3. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/pyproject.toml +1 -1
  4. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/__init__.py +1 -1
  5. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/audiotools.py +38 -18
  6. quicktools_atom-0.7.3/src/quicktools/presentationtools.py +294 -0
  7. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools_atom.egg-info/PKG-INFO +2 -2
  8. quicktools_atom-0.7.1/src/quicktools/presentationtools.py +0 -249
  9. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/setup.cfg +0 -0
  10. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/calculus.py +0 -0
  11. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/cli.py +0 -0
  12. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/combinatorics.py +0 -0
  13. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/doctools.py +0 -0
  14. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/filetools.py +0 -0
  15. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/imagetools.py +0 -0
  16. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/linalg.py +0 -0
  17. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/mathtools.py +0 -0
  18. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/numbertheory.py +0 -0
  19. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/pdftools.py +0 -0
  20. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/spreadsheettools.py +0 -0
  21. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/strtools.py +0 -0
  22. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/unitconverter.py +0 -0
  23. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools/videotools.py +0 -0
  24. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools_atom.egg-info/SOURCES.txt +0 -0
  25. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools_atom.egg-info/dependency_links.txt +0 -0
  26. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools_atom.egg-info/entry_points.txt +0 -0
  27. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools_atom.egg-info/requires.txt +0 -0
  28. {quicktools_atom-0.7.1 → quicktools_atom-0.7.3}/src/quicktools_atom.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quicktools-atom
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: Math, text, presentation, audio, video, and file utilities by AtomDev Studios
5
5
  Author-email: Samuel Peprah <windscribe.samuel@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Samuel-Peprah-cmd/quicktools
@@ -205,5 +205,5 @@ PostgreSQL • SQLite • Git • GitHub • Cloudflare • Docker
205
205
 
206
206
 
207
207
 
208
- \_\_version\_\_ = "0.7.0"
208
+ \_\_version\_\_ = "0.7.3"
209
209
 
@@ -182,5 +182,5 @@ PostgreSQL • SQLite • Git • GitHub • Cloudflare • Docker
182
182
 
183
183
 
184
184
 
185
- \_\_version\_\_ = "0.7.0"
185
+ \_\_version\_\_ = "0.7.3"
186
186
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quicktools-atom"
7
- version = "0.7.1"
7
+ version = "0.7.3"
8
8
  description = "Math, text, presentation, audio, video, and file utilities by AtomDev Studios"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -92,4 +92,4 @@ PostgreSQL • SQLite • Git • GitHub • Cloudflare • Docker
92
92
  - `videotools` — video transcription, frame extraction, speaker diarization to Word scripts
93
93
  """
94
94
 
95
- __version__ = "0.7.0"
95
+ __version__ = "0.7.3"
@@ -75,11 +75,39 @@ def save_transcript_as_srt(segments: list[dict], output_path: str) -> None:
75
75
 
76
76
  # --- NEW DIARIZATION AND STYLING FEATURES ---
77
77
 
78
+ def _load_waveform_via_av(path: str, target_sr: int = 16000):
79
+ """Decode any audio/video file into a mono waveform tensor using PyAV — bypassing
80
+ pyannote's own (fragile, Windows-unfriendly) torchcodec-based audio loader entirely."""
81
+ import av
82
+ import torch
83
+ import numpy as np
84
+
85
+ container = av.open(path)
86
+ resampler = av.AudioResampler(format="s16", layout="mono", rate=target_sr)
87
+ audio_stream = container.streams.audio[0]
88
+
89
+ chunks = []
90
+ for frame in container.decode(audio_stream):
91
+ for resampled in resampler.resample(frame):
92
+ chunks.append(resampled.to_ndarray())
93
+ container.close()
94
+
95
+ if not chunks:
96
+ raise RuntimeError(f"No audio could be decoded from '{path}'")
97
+
98
+ audio = np.concatenate(chunks, axis=1).astype(np.float32) / 32768.0 # int16 -> [-1, 1]
99
+ waveform = torch.from_numpy(audio) # shape: (1, num_samples) since layout="mono"
100
+ return waveform, target_sr
101
+
102
+
78
103
  def transcribe_with_speakers(path: str, hf_token: str, model_size: str = "base") -> list[dict]:
79
- """
80
- Transcribes audio and aligns the text with Speaker IDs using Pyannote.
104
+ """Transcribe audio and label each segment with which speaker said it (speaker diarization).
81
105
  Returns a list of dicts: [{"start": 0.0, "end": 5.0, "speaker": "Speaker 00", "text": "Hello"}]
82
- Requires a Hugging Face token with access to pyannote/speaker-diarization-3.1.
106
+
107
+ Requires:
108
+ - pip install pyannote.audio
109
+ - A free Hugging Face account + access token (huggingface.co/settings/tokens)
110
+ - Accepting the model terms at huggingface.co/pyannote/speaker-diarization-3.1
83
111
  """
84
112
  try:
85
113
  from pyannote.audio import Pipeline
@@ -88,38 +116,30 @@ def transcribe_with_speakers(path: str, hf_token: str, model_size: str = "base")
88
116
  "Speaker diarization requires pyannote.audio. Install it with: pip install pyannote.audio"
89
117
  )
90
118
 
91
- print("1/3: Analyzing voice biometrics (Diarization)...")
92
- pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1", use_auth_token=hf_token)
93
- diarization = pipeline(path)
119
+ waveform, sample_rate = _load_waveform_via_av(path)
120
+
121
+ pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1", token=hf_token)
122
+ diarization = pipeline({"waveform": waveform, "sample_rate": sample_rate})
94
123
 
95
- print("2/3: Transcribing audio text...")
96
124
  model = _load_model(model_size)
97
125
  segments, _ = model.transcribe(path)
98
126
 
99
- print("3/3: Aligning text to speakers...")
100
127
  transcript_data = []
101
-
102
128
  for segment in segments:
103
- # Find the midpoint of the spoken phrase
104
- segment_midpoint = (segment.start + segment.end) / 2
105
-
106
- # Check which speaker was talking at that exact millisecond
129
+ midpoint = (segment.start + segment.end) / 2
107
130
  current_speaker = "Unknown"
108
131
  for turn, _, speaker in diarization.itertracks(yield_label=True):
109
- if turn.start <= segment_midpoint <= turn.end:
132
+ if turn.start <= midpoint <= turn.end:
110
133
  current_speaker = speaker
111
134
  break
112
-
113
135
  transcript_data.append({
114
136
  "start": segment.start,
115
137
  "end": segment.end,
116
138
  "speaker": current_speaker.replace("SPEAKER_", "Speaker "),
117
- "text": segment.text.strip()
139
+ "text": segment.text.strip(),
118
140
  })
119
-
120
141
  return transcript_data
121
142
 
122
-
123
143
  def save_transcript_to_docx(transcript_data: list[dict], output_path: str) -> None:
124
144
  """
125
145
  Takes speaker-mapped transcript data and generates a beautifully styled Word document.
@@ -0,0 +1,294 @@
1
+ """PowerPoint utilities: reading, creating .pptx files, and content-based conversion
2
+ to/from Word documents and PDFs — with styled themes and image placement.
3
+
4
+ Note on conversions: Word docs, PDFs, and PowerPoint decks use fundamentally different
5
+ document models (flowing paragraphs vs. discrete slides). These functions preserve and
6
+ reorganize the actual text content and images sensibly into the new format — they do not
7
+ attempt to replicate exact visual layout, because no such 1:1 mapping exists between formats.
8
+
9
+ Themes are applied entirely in code (background/title/accent colors) — no external
10
+ template files or network downloads are required.
11
+ """
12
+ import io
13
+ import random
14
+ from pptx import Presentation
15
+ from pptx.util import Inches, Pt
16
+ from pptx.dml.color import RGBColor
17
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
18
+
19
+ # Each theme is a self-contained color palette applied entirely in code.
20
+ THEMES = [
21
+ {"name": "Ocean Blue", "background": RGBColor(0x0B, 0x2A, 0x4A), "title": RGBColor(0xFF, 0xFF, 0xFF), "accent": RGBColor(0x9C, 0xD3, 0xFF)},
22
+ {"name": "Sunset Orange", "background": RGBColor(0x3A, 0x1A, 0x0A), "title": RGBColor(0xFF, 0xD9, 0xA0), "accent": RGBColor(0xFF, 0xFF, 0xFF)},
23
+ {"name": "Forest Green", "background": RGBColor(0x0E, 0x2E, 0x1D), "title": RGBColor(0xFF, 0xFF, 0xFF), "accent": RGBColor(0xA8, 0xE6, 0xB8)},
24
+ {"name": "Monochrome", "background": RGBColor(0x1A, 0x1A, 0x1A), "title": RGBColor(0xFF, 0xFF, 0xFF), "accent": RGBColor(0xCC, 0xCC, 0xCC)},
25
+ {"name": "Soft Light", "background": RGBColor(0xF5, 0xF5, 0xF0), "title": RGBColor(0x22, 0x22, 0x22), "accent": RGBColor(0x44, 0x44, 0x44)},
26
+ ]
27
+
28
+ class Theme:
29
+ """Pre-defined theme constants for easy IDE auto-completion."""
30
+ OCEAN_BLUE = "Ocean Blue"
31
+ SUNSET_ORANGE = "Sunset Orange"
32
+ FOREST_GREEN = "Forest Green"
33
+ MONOCHROME = "Monochrome"
34
+ SOFT_LIGHT = "Soft Light"
35
+
36
+
37
+ def _pick_theme(theme_name: str | None = None) -> dict:
38
+ """Return a theme dict by name, or a random one if no name is given."""
39
+ if theme_name:
40
+ for t in THEMES:
41
+ if t["name"].lower() == theme_name.lower():
42
+ return t
43
+ raise ValueError(f"Unknown theme '{theme_name}'. Available: {[t['name'] for t in THEMES]}")
44
+ return random.choice(THEMES)
45
+
46
+
47
+ def _apply_background(slide, theme: dict) -> None:
48
+ """Fill a slide's background with the theme's background color."""
49
+ fill = slide.background.fill
50
+ fill.solid()
51
+ fill.fore_color.rgb = theme["background"]
52
+
53
+
54
+ def _add_slide_with_text(prs: Presentation, theme: dict, title: str, body_lines: list[str],
55
+ image_path: str | None = None) -> None:
56
+ """Internal helper: add a themed slide with a title, bullet text, and an optional image."""
57
+ blank_layout = prs.slide_layouts[6]
58
+ slide = prs.slides.add_slide(blank_layout)
59
+ _apply_background(slide, theme)
60
+
61
+ has_image = image_path is not None
62
+ text_width = Inches(4.7) if has_image else Inches(9)
63
+
64
+ title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), text_width, Inches(1))
65
+ tf = title_box.text_frame
66
+ tf.word_wrap = True
67
+ tf.text = title
68
+ tf.paragraphs[0].font.size = Pt(28)
69
+ tf.paragraphs[0].font.bold = True
70
+ tf.paragraphs[0].font.color.rgb = theme["title"]
71
+
72
+ body_box = slide.shapes.add_textbox(Inches(0.5), Inches(1.5), text_width, Inches(5.3))
73
+ body_tf = body_box.text_frame
74
+ body_tf.word_wrap = True
75
+ for i, line in enumerate(body_lines):
76
+ p = body_tf.paragraphs[0] if i == 0 else body_tf.add_paragraph()
77
+ p.text = f"• {line}"
78
+ p.font.size = Pt(18)
79
+ p.font.color.rgb = theme["accent"]
80
+
81
+ if has_image:
82
+ slide.shapes.add_picture(image_path, Inches(5.5), Inches(1.5), width=Inches(4), height=Inches(4.5))
83
+
84
+
85
+ def read_pptx_text(path: str) -> str:
86
+ """Extract all text from every slide in a .pptx file."""
87
+ prs = Presentation(path)
88
+ lines = []
89
+ for slide in prs.slides:
90
+ for shape in slide.shapes:
91
+ if shape.has_text_frame:
92
+ for paragraph in shape.text_frame.paragraphs:
93
+ text = "".join(run.text for run in paragraph.runs)
94
+ if text:
95
+ lines.append(text)
96
+ return "\n".join(lines)
97
+
98
+
99
+ def get_pptx_slide_count(path: str) -> int:
100
+ """Return the number of slides in a .pptx file."""
101
+ return len(Presentation(path).slides)
102
+
103
+
104
+ def list_available_themes() -> list[str]:
105
+ """Return the names of all available built-in themes."""
106
+ return [t["name"] for t in THEMES]
107
+
108
+
109
+ def create_pptx_from_text(text: str, output_path: str, theme_name: str | None = None) -> None:
110
+ """Create a themed .pptx file from text, splitting on blank lines into separate slides.
111
+ Each slide's first line becomes its title; remaining lines become bullet content.
112
+ theme_name picks a specific theme (see list_available_themes()); omit for a random one."""
113
+ theme = _pick_theme(theme_name)
114
+ prs = Presentation()
115
+ blocks = [b.strip() for b in text.split("\n\n") if b.strip()]
116
+ if not blocks:
117
+ blocks = [text]
118
+ for block in blocks:
119
+ lines = [l for l in block.split("\n") if l.strip()]
120
+ title = lines[0] if lines else "Untitled"
121
+ body = lines[1:] if len(lines) > 1 else []
122
+ _add_slide_with_text(prs, theme, title, body)
123
+ prs.save(output_path)
124
+
125
+
126
+ def _looks_like_heading(paragraph) -> bool:
127
+ """Heuristic: is this docx paragraph likely meant as a section heading?
128
+ True Word heading styles always count. As a fallback (since many real documents never
129
+ use proper Word styles), a short, fully-bolded line with no trailing period is also
130
+ treated as a heading."""
131
+ style_name = paragraph.style.name.lower()
132
+ if style_name.startswith("heading") or style_name == "title":
133
+ return True
134
+ text = paragraph.text.strip()
135
+ if not text or len(text) > 60 or text.endswith((".", ",", ";")):
136
+ return False
137
+ runs = [r for r in paragraph.runs if r.text.strip()]
138
+ return bool(runs) and all(r.bold for r in runs)
139
+
140
+
141
+ def _extract_paragraph_images(paragraph, doc) -> list[bytes]:
142
+ """Extract raw image bytes for any images embedded directly in this docx paragraph."""
143
+ from docx.oxml.ns import qn
144
+ images = []
145
+ for run in paragraph.runs:
146
+ for blip in run._element.findall(".//" + qn("a:blip")):
147
+ rId = blip.get(qn("r:embed"))
148
+ if rId and rId in doc.part.rels:
149
+ images.append(doc.part.rels[rId].target_part.blob)
150
+ return images
151
+
152
+
153
+ def pptx_to_docx(path: str, output_path: str) -> None:
154
+ """Convert a .pptx file to .docx: each slide's title becomes a heading, its text becomes
155
+ paragraphs, and any picture on the slide is inserted into the document."""
156
+ from docx import Document
157
+ prs = Presentation(path)
158
+ doc = Document()
159
+ for i, slide in enumerate(prs.slides):
160
+ texts = []
161
+ image_blobs = []
162
+ for shape in slide.shapes:
163
+ if shape.has_text_frame:
164
+ for paragraph in shape.text_frame.paragraphs:
165
+ line = "".join(run.text for run in paragraph.runs)
166
+ if line:
167
+ texts.append(line)
168
+ if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
169
+ image_blobs.append(shape.image.blob)
170
+ if not texts and not image_blobs:
171
+ continue
172
+ if texts:
173
+ doc.add_heading(texts[0], level=1)
174
+ for line in texts[1:]:
175
+ doc.add_paragraph(line)
176
+ for blob in image_blobs:
177
+ doc.add_picture(io.BytesIO(blob), width=Inches(4))
178
+ if i < len(prs.slides) - 1:
179
+ doc.add_page_break()
180
+ doc.save(output_path)
181
+
182
+
183
+ def docx_to_pptx(path: str, output_path: str, theme_name: str | None = None) -> None:
184
+ """Convert a .docx file to a themed .pptx: headings (or bolded short lines) start new
185
+ slides, normal paragraphs become bullet content, and any embedded image is placed
186
+ on its corresponding slide. theme_name picks a specific theme; omit for a random one."""
187
+ from docx import Document
188
+ import tempfile
189
+
190
+ doc = Document(path)
191
+ theme = _pick_theme(theme_name)
192
+ prs = Presentation()
193
+
194
+ current_title = None
195
+ current_body: list[str] = []
196
+ current_image: str | None = None
197
+
198
+ def flush():
199
+ if current_title is not None:
200
+ _add_slide_with_text(prs, theme, current_title, current_body, current_image)
201
+
202
+ for p in doc.paragraphs:
203
+ text = p.text.strip()
204
+ images = _extract_paragraph_images(p, doc)
205
+ image_path = None
206
+ if images:
207
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
208
+ tmp.write(images[0])
209
+ tmp.close()
210
+ image_path = tmp.name
211
+
212
+ if not text and not image_path:
213
+ continue
214
+
215
+ if text and _looks_like_heading(p):
216
+ flush()
217
+ current_title = text
218
+ current_body = []
219
+ current_image = image_path
220
+ else:
221
+ if current_title is None:
222
+ current_title = text or "Untitled"
223
+ elif text:
224
+ current_body.append(text)
225
+ if image_path and current_image is None:
226
+ current_image = image_path
227
+ flush()
228
+
229
+ prs.save(output_path)
230
+
231
+
232
+ def pdf_to_pptx(path: str, output_path: str, theme_name: str | None = None) -> None:
233
+ """Convert a PDF to a themed .pptx: each page's extracted text and first embedded
234
+ image (if any) are placed onto their own slide. theme_name picks a specific theme;
235
+ omit for a random one."""
236
+ from pypdf import PdfReader
237
+ import tempfile
238
+
239
+ reader = PdfReader(path)
240
+ theme = _pick_theme(theme_name)
241
+ prs = Presentation()
242
+
243
+ for i, page in enumerate(reader.pages):
244
+ text = page.extract_text() or ""
245
+ lines = [l for l in text.split("\n") if l.strip()]
246
+ title = lines[0] if lines else f"Page {i + 1}"
247
+ body = lines[1:] if len(lines) > 1 else []
248
+
249
+ image_path = None
250
+ try:
251
+ images = page.images
252
+ if len(images) > 0:
253
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
254
+ images[0].image.save(tmp.name)
255
+ tmp.close()
256
+ image_path = tmp.name
257
+ except Exception:
258
+ image_path = None
259
+
260
+ _add_slide_with_text(prs, theme, title, body, image_path)
261
+
262
+ prs.save(output_path)
263
+
264
+
265
+ def extract_pptx_notes(path: str) -> list[str]:
266
+ """Extract the speaker notes from every slide in a .pptx file."""
267
+ prs = Presentation(path)
268
+ notes = []
269
+ for slide in prs.slides:
270
+ if slide.has_notes_slide:
271
+ notes.append(slide.notes_slide.notes_text_frame.text)
272
+ else:
273
+ notes.append("")
274
+ return notes
275
+
276
+
277
+ def merge_pptx_files(paths: list[str], output_path: str, theme_name: str | None = None) -> None:
278
+ """Merge multiple .pptx files' content into one themed deck, preserving slide order."""
279
+ theme = _pick_theme(theme_name)
280
+ prs = Presentation()
281
+ for path in paths:
282
+ source = Presentation(path)
283
+ for slide in source.slides:
284
+ texts = []
285
+ for shape in slide.shapes:
286
+ if shape.has_text_frame:
287
+ for paragraph in shape.text_frame.paragraphs:
288
+ line = "".join(run.text for run in paragraph.runs)
289
+ if line:
290
+ texts.append(line)
291
+ title = texts[0] if texts else "Untitled"
292
+ body = texts[1:] if len(texts) > 1 else []
293
+ _add_slide_with_text(prs, theme, title, body)
294
+ prs.save(output_path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quicktools-atom
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: Math, text, presentation, audio, video, and file utilities by AtomDev Studios
5
5
  Author-email: Samuel Peprah <windscribe.samuel@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Samuel-Peprah-cmd/quicktools
@@ -205,5 +205,5 @@ PostgreSQL • SQLite • Git • GitHub • Cloudflare • Docker
205
205
 
206
206
 
207
207
 
208
- \_\_version\_\_ = "0.7.0"
208
+ \_\_version\_\_ = "0.7.3"
209
209
 
@@ -1,249 +0,0 @@
1
- """PowerPoint utilities: reading, creating .pptx files, and smart content-based conversion
2
- to/from Word documents and PDFs.
3
-
4
- This module features a Semantic Layout Engine: when converting from Word or text,
5
- it automatically detects Headings to create Slide Titles, formats normal text as
6
- bullet points, and dynamically snaps images into professional slide layouts.
7
- """
8
- import os
9
- import random
10
- import tempfile
11
- import urllib.request
12
- from pptx import Presentation
13
- from pptx.util import Inches, Pt
14
-
15
- CLOUD_THEMES = [
16
- "https://raw.githubusercontent.com/Samuel-Peprah-cmd/quicktools/main/assets/themes/modern_blue.pptx",
17
- "https://raw.githubusercontent.com/Samuel-Peprah-cmd/quicktools/main/assets/themes/dark_minimal.pptx",
18
- "https://raw.githubusercontent.com/Samuel-Peprah-cmd/quicktools/main/assets/themes/creative_orange.pptx"
19
- ]
20
-
21
- def _get_template_path(template_target: str | None) -> str | None:
22
- """Internal helper: Resolves a template file, a local directory, or fetches a cloud theme."""
23
- if not template_target:
24
- return None
25
-
26
- # --- NEW: Cloud Fetching ---
27
- if template_target.lower() == "random_cloud":
28
- print("Fetching a random theme from the cloud...")
29
- url = random.choice(CLOUD_THEMES)
30
- try:
31
- # Create a temporary file that deletes itself when the script finishes
32
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pptx")
33
- urllib.request.urlretrieve(url, temp_file.name)
34
- return temp_file.name
35
- except Exception as e:
36
- print(f"Warning: Could not fetch cloud theme ({e}). Falling back to blank template.")
37
- return None
38
-
39
- # --- Local File/Folder Logic ---
40
- if os.path.isfile(template_target) and template_target.endswith('.pptx'):
41
- return template_target
42
-
43
- if os.path.isdir(template_target):
44
- templates = [os.path.join(template_target, f) for f in os.listdir(template_target) if f.endswith('.pptx')]
45
- if templates:
46
- return random.choice(templates)
47
-
48
- return None
49
-
50
- def read_pptx_text(path: str) -> str:
51
- """Extract all text from every slide in a .pptx file."""
52
- prs = Presentation(path)
53
- lines = []
54
- for slide in prs.slides:
55
- for shape in slide.shapes:
56
- if shape.has_text_frame:
57
- for paragraph in shape.text_frame.paragraphs:
58
- text = "".join(run.text for run in paragraph.runs)
59
- if text:
60
- lines.append(text)
61
- return "\n".join(lines)
62
-
63
-
64
- def get_pptx_slide_count(path: str) -> int:
65
- """Return the number of slides in a .pptx file."""
66
- return len(Presentation(path).slides)
67
-
68
-
69
- def create_smart_pptx(slide_data: list[dict], output_path: str, template_target: str | None = None) -> None:
70
- """
71
- Creates a formatted PowerPoint presentation from structured data.
72
- slide_data format: [{"title": "Slide 1", "content": ["Point 1", "Point 2"], "image": "path.jpg"}]
73
-
74
- If template_target is a directory, a random .pptx theme from that directory is applied.
75
- """
76
- theme = _get_template_path(template_target)
77
- prs = Presentation(theme) if theme else Presentation()
78
-
79
- # Standard Layout indices: 0 = Title Slide, 1 = Title & Content, 8 = Picture with Caption
80
- title_slide_layout = prs.slide_layouts[0]
81
-
82
- # Fallbacks in case the custom template is structured unusually
83
- try:
84
- content_slide_layout = prs.slide_layouts[1]
85
- except IndexError:
86
- content_slide_layout = prs.slide_layouts[0]
87
-
88
- for i, slide_dict in enumerate(slide_data):
89
- title = slide_dict.get("title", "Untitled Slide")
90
- content = slide_dict.get("content", [])
91
- image_path = slide_dict.get("image", None)
92
-
93
- # Use Title Slide layout for the very first slide if it has no bullets
94
- if i == 0 and not content and not image_path:
95
- slide = prs.slides.add_slide(title_slide_layout)
96
- if slide.shapes.title:
97
- slide.shapes.title.text = title
98
- if len(slide.placeholders) > 1:
99
- slide.placeholders[1].text = "Generated by quicktools"
100
- continue
101
-
102
- # Create a standard slide
103
- slide = prs.slides.add_slide(content_slide_layout)
104
-
105
- # Set Title
106
- if slide.shapes.title:
107
- slide.shapes.title.text = title
108
-
109
- # Set Bullet Points
110
- if len(slide.placeholders) > 1:
111
- body_shape = slide.placeholders[1]
112
- tf = body_shape.text_frame
113
-
114
- for idx, point in enumerate(content):
115
- p = tf.paragraphs[0] if idx == 0 else tf.add_paragraph()
116
- p.text = point
117
- p.font.size = Pt(20)
118
-
119
- # Smart Image Placement Strategy
120
- if image_path and os.path.exists(image_path):
121
- # Shrink the text box to make room for the image on the right
122
- body_shape.width = Inches(5)
123
-
124
- # Snap image to the right side of a standard 10x7.5 widescreen slide
125
- left_position = Inches(5.5)
126
- top_position = Inches(2)
127
- image_height = Inches(4.5)
128
-
129
- slide.shapes.add_picture(image_path, left_position, top_position, height=image_height)
130
-
131
- prs.save(output_path)
132
-
133
-
134
- def create_pptx_from_text(text: str, output_path: str, template_target: str | None = None) -> None:
135
- """Create a .pptx file from text, splitting on blank lines into separate slides.
136
- Routes to the smart layout engine."""
137
- blocks = [b.strip() for b in text.split("\n\n") if b.strip()]
138
- if not blocks:
139
- blocks = [text]
140
-
141
- slide_data = []
142
- for block in blocks:
143
- lines = [l for l in block.split("\n") if l.strip()]
144
- title = lines[0] if lines else "Untitled"
145
- body = lines[1:] if len(lines) > 1 else []
146
- slide_data.append({"title": title, "content": body})
147
-
148
- create_smart_pptx(slide_data, output_path, template_target)
149
-
150
-
151
- def pptx_to_docx(path: str, output_path: str) -> None:
152
- """Convert a .pptx file to .docx, using each slide's text as a section."""
153
- from docx import Document
154
- prs = Presentation(path)
155
- doc = Document()
156
- for i, slide in enumerate(prs.slides):
157
- texts = []
158
- for shape in slide.shapes:
159
- if shape.has_text_frame:
160
- for paragraph in shape.text_frame.paragraphs:
161
- line = "".join(run.text for run in paragraph.runs)
162
- if line:
163
- texts.append(line)
164
- if not texts:
165
- continue
166
- doc.add_heading(texts[0], level=1)
167
- for line in texts[1:]:
168
- doc.add_paragraph(line)
169
- if i < len(prs.slides) - 1:
170
- doc.add_page_break()
171
- doc.save(output_path)
172
-
173
-
174
- def docx_to_pptx(path: str, output_path: str, template_target: str | None = None) -> None:
175
- """Convert a .docx file to .pptx using semantic parsing.
176
- Headings become Slide Titles. Normal text becomes bullet points."""
177
- from docx import Document
178
- doc = Document(path)
179
- slide_data = []
180
- current_slide = None
181
-
182
- for p in doc.paragraphs:
183
- text = p.text.strip()
184
- if not text:
185
- continue
186
-
187
- is_heading = p.style.name.lower().startswith("heading") or p.style.name.lower() == "title"
188
-
189
- if is_heading:
190
- if current_slide:
191
- slide_data.append(current_slide)
192
- current_slide = {"title": text, "content": []}
193
- else:
194
- if current_slide is None:
195
- current_slide = {"title": text, "content": []}
196
- else:
197
- current_slide["content"].append(text)
198
-
199
- if current_slide:
200
- slide_data.append(current_slide)
201
-
202
- create_smart_pptx(slide_data, output_path, template_target)
203
-
204
-
205
- def pdf_to_pptx(path: str, output_path: str, template_target: str | None = None) -> None:
206
- """Convert a PDF to .pptx, placing each PDF page's extracted text onto its own slide."""
207
- from pypdf import PdfReader
208
- reader = PdfReader(path)
209
- slide_data = []
210
-
211
- for i, page in enumerate(reader.pages):
212
- text = page.extract_text() or ""
213
- lines = [l for l in text.split("\n") if l.strip()]
214
- title = f"Page {i + 1}"
215
- slide_data.append({"title": title, "content": lines})
216
-
217
- create_smart_pptx(slide_data, output_path, template_target)
218
-
219
-
220
- def extract_pptx_notes(path: str) -> list[str]:
221
- """Extract the speaker notes from every slide in a .pptx file."""
222
- prs = Presentation(path)
223
- notes = []
224
- for slide in prs.slides:
225
- if slide.has_notes_slide:
226
- notes.append(slide.notes_slide.notes_text_frame.text)
227
- else:
228
- notes.append("")
229
- return notes
230
-
231
-
232
- def merge_pptx_files(paths: list[str], output_path: str) -> None:
233
- """Merge multiple .pptx files' content into one, preserving slide order across files."""
234
- slide_data = []
235
- for path in paths:
236
- source = Presentation(path)
237
- for slide in source.slides:
238
- texts = []
239
- for shape in slide.shapes:
240
- if shape.has_text_frame:
241
- for paragraph in shape.text_frame.paragraphs:
242
- line = "".join(run.text for run in paragraph.runs)
243
- if line:
244
- texts.append(line)
245
- title = texts[0] if texts else "Untitled"
246
- body = texts[1:] if len(texts) > 1 else []
247
- slide_data.append({"title": title, "content": body})
248
-
249
- create_smart_pptx(slide_data, output_path)