quicktools-atom 0.7.0__tar.gz → 0.7.2__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.
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/PKG-INFO +7 -5
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/README.md +2 -4
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/pyproject.toml +6 -2
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/__init__.py +2 -2
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/audiotools.py +38 -18
- quicktools_atom-0.7.2/src/quicktools/presentationtools.py +286 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools_atom.egg-info/PKG-INFO +7 -5
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools_atom.egg-info/requires.txt +5 -0
- quicktools_atom-0.7.0/src/quicktools/presentationtools.py +0 -249
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/setup.cfg +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/calculus.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/cli.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/combinatorics.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/doctools.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/filetools.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/imagetools.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/linalg.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/mathtools.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/numbertheory.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/pdftools.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/spreadsheettools.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/strtools.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/unitconverter.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools/videotools.py +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools_atom.egg-info/SOURCES.txt +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools_atom.egg-info/dependency_links.txt +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools_atom.egg-info/entry_points.txt +0 -0
- {quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/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.
|
|
3
|
+
Version: 0.7.2
|
|
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
|
|
@@ -20,13 +20,16 @@ Requires-Dist: python-pptx
|
|
|
20
20
|
Requires-Dist: faster-whisper
|
|
21
21
|
Requires-Dist: av
|
|
22
22
|
Requires-Dist: pyannote.audio
|
|
23
|
+
Requires-Dist: torch
|
|
24
|
+
Provides-Extra: diarization
|
|
25
|
+
Requires-Dist: pyannote.audio; extra == "diarization"
|
|
26
|
+
Requires-Dist: torch; extra == "diarization"
|
|
23
27
|
|
|
24
28
|
"""
|
|
25
29
|
|
|
26
30
|
<p align="center">
|
|
27
31
|
|
|
28
|
-
 
|
|
29
|
-
|
|
32
|
+
  
|
|
30
33
|
</p>
|
|
31
34
|
|
|
32
35
|
|
|
@@ -51,8 +54,7 @@ under \*\*AtomDev Studios\*\*.
|
|
|
51
54
|
|
|
52
55
|
<p align="center">
|
|
53
56
|
|
|
54
|
-
 
|
|
55
|
-
|
|
57
|
+
  
|
|
56
58
|
</p>
|
|
57
59
|
|
|
58
60
|
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
|
|
5
|
-
 
|
|
6
|
-
|
|
5
|
+
  
|
|
7
6
|
</p>
|
|
8
7
|
|
|
9
8
|
|
|
@@ -28,8 +27,7 @@ under \*\*AtomDev Studios\*\*.
|
|
|
28
27
|
|
|
29
28
|
<p align="center">
|
|
30
29
|
|
|
31
|
-
 
|
|
32
|
-
|
|
30
|
+
  
|
|
33
31
|
</p>
|
|
34
32
|
|
|
35
33
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quicktools-atom"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
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"
|
|
@@ -24,6 +24,7 @@ dependencies = [
|
|
|
24
24
|
"faster-whisper",
|
|
25
25
|
"av",
|
|
26
26
|
"pyannote.audio",
|
|
27
|
+
"torch",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[project.urls]
|
|
@@ -35,4 +36,7 @@ Documentation = "https://samuel-peprah-cmd.github.io/quicktools/"
|
|
|
35
36
|
quicktools = "quicktools.cli:main"
|
|
36
37
|
|
|
37
38
|
[tool.setuptools.packages.find]
|
|
38
|
-
where = ["src"]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
diarization = ["pyannote.audio", "torch"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
<p align="center">
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
</p>
|
|
5
5
|
|
|
6
6
|
# quicktools
|
|
@@ -13,7 +13,7 @@ under **AtomDev Studios**.
|
|
|
13
13
|
## About Samuel Peprah
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
|
|
16
|
+

|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
Samuel Peprah is a software developer, web application developer, and
|
|
@@ -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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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 <=
|
|
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,286 @@
|
|
|
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
|
+
|
|
29
|
+
def _pick_theme(theme_name: str | None = None) -> dict:
|
|
30
|
+
"""Return a theme dict by name, or a random one if no name is given."""
|
|
31
|
+
if theme_name:
|
|
32
|
+
for t in THEMES:
|
|
33
|
+
if t["name"].lower() == theme_name.lower():
|
|
34
|
+
return t
|
|
35
|
+
raise ValueError(f"Unknown theme '{theme_name}'. Available: {[t['name'] for t in THEMES]}")
|
|
36
|
+
return random.choice(THEMES)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _apply_background(slide, theme: dict) -> None:
|
|
40
|
+
"""Fill a slide's background with the theme's background color."""
|
|
41
|
+
fill = slide.background.fill
|
|
42
|
+
fill.solid()
|
|
43
|
+
fill.fore_color.rgb = theme["background"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _add_slide_with_text(prs: Presentation, theme: dict, title: str, body_lines: list[str],
|
|
47
|
+
image_path: str | None = None) -> None:
|
|
48
|
+
"""Internal helper: add a themed slide with a title, bullet text, and an optional image."""
|
|
49
|
+
blank_layout = prs.slide_layouts[6]
|
|
50
|
+
slide = prs.slides.add_slide(blank_layout)
|
|
51
|
+
_apply_background(slide, theme)
|
|
52
|
+
|
|
53
|
+
has_image = image_path is not None
|
|
54
|
+
text_width = Inches(4.7) if has_image else Inches(9)
|
|
55
|
+
|
|
56
|
+
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), text_width, Inches(1))
|
|
57
|
+
tf = title_box.text_frame
|
|
58
|
+
tf.word_wrap = True
|
|
59
|
+
tf.text = title
|
|
60
|
+
tf.paragraphs[0].font.size = Pt(28)
|
|
61
|
+
tf.paragraphs[0].font.bold = True
|
|
62
|
+
tf.paragraphs[0].font.color.rgb = theme["title"]
|
|
63
|
+
|
|
64
|
+
body_box = slide.shapes.add_textbox(Inches(0.5), Inches(1.5), text_width, Inches(5.3))
|
|
65
|
+
body_tf = body_box.text_frame
|
|
66
|
+
body_tf.word_wrap = True
|
|
67
|
+
for i, line in enumerate(body_lines):
|
|
68
|
+
p = body_tf.paragraphs[0] if i == 0 else body_tf.add_paragraph()
|
|
69
|
+
p.text = f"• {line}"
|
|
70
|
+
p.font.size = Pt(18)
|
|
71
|
+
p.font.color.rgb = theme["accent"]
|
|
72
|
+
|
|
73
|
+
if has_image:
|
|
74
|
+
slide.shapes.add_picture(image_path, Inches(5.5), Inches(1.5), width=Inches(4), height=Inches(4.5))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def read_pptx_text(path: str) -> str:
|
|
78
|
+
"""Extract all text from every slide in a .pptx file."""
|
|
79
|
+
prs = Presentation(path)
|
|
80
|
+
lines = []
|
|
81
|
+
for slide in prs.slides:
|
|
82
|
+
for shape in slide.shapes:
|
|
83
|
+
if shape.has_text_frame:
|
|
84
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
85
|
+
text = "".join(run.text for run in paragraph.runs)
|
|
86
|
+
if text:
|
|
87
|
+
lines.append(text)
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_pptx_slide_count(path: str) -> int:
|
|
92
|
+
"""Return the number of slides in a .pptx file."""
|
|
93
|
+
return len(Presentation(path).slides)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def list_available_themes() -> list[str]:
|
|
97
|
+
"""Return the names of all available built-in themes."""
|
|
98
|
+
return [t["name"] for t in THEMES]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_pptx_from_text(text: str, output_path: str, theme_name: str | None = None) -> None:
|
|
102
|
+
"""Create a themed .pptx file from text, splitting on blank lines into separate slides.
|
|
103
|
+
Each slide's first line becomes its title; remaining lines become bullet content.
|
|
104
|
+
theme_name picks a specific theme (see list_available_themes()); omit for a random one."""
|
|
105
|
+
theme = _pick_theme(theme_name)
|
|
106
|
+
prs = Presentation()
|
|
107
|
+
blocks = [b.strip() for b in text.split("\n\n") if b.strip()]
|
|
108
|
+
if not blocks:
|
|
109
|
+
blocks = [text]
|
|
110
|
+
for block in blocks:
|
|
111
|
+
lines = [l for l in block.split("\n") if l.strip()]
|
|
112
|
+
title = lines[0] if lines else "Untitled"
|
|
113
|
+
body = lines[1:] if len(lines) > 1 else []
|
|
114
|
+
_add_slide_with_text(prs, theme, title, body)
|
|
115
|
+
prs.save(output_path)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _looks_like_heading(paragraph) -> bool:
|
|
119
|
+
"""Heuristic: is this docx paragraph likely meant as a section heading?
|
|
120
|
+
True Word heading styles always count. As a fallback (since many real documents never
|
|
121
|
+
use proper Word styles), a short, fully-bolded line with no trailing period is also
|
|
122
|
+
treated as a heading."""
|
|
123
|
+
style_name = paragraph.style.name.lower()
|
|
124
|
+
if style_name.startswith("heading") or style_name == "title":
|
|
125
|
+
return True
|
|
126
|
+
text = paragraph.text.strip()
|
|
127
|
+
if not text or len(text) > 60 or text.endswith((".", ",", ";")):
|
|
128
|
+
return False
|
|
129
|
+
runs = [r for r in paragraph.runs if r.text.strip()]
|
|
130
|
+
return bool(runs) and all(r.bold for r in runs)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _extract_paragraph_images(paragraph, doc) -> list[bytes]:
|
|
134
|
+
"""Extract raw image bytes for any images embedded directly in this docx paragraph."""
|
|
135
|
+
from docx.oxml.ns import qn
|
|
136
|
+
images = []
|
|
137
|
+
for run in paragraph.runs:
|
|
138
|
+
for blip in run._element.findall(".//" + qn("a:blip")):
|
|
139
|
+
rId = blip.get(qn("r:embed"))
|
|
140
|
+
if rId and rId in doc.part.rels:
|
|
141
|
+
images.append(doc.part.rels[rId].target_part.blob)
|
|
142
|
+
return images
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def pptx_to_docx(path: str, output_path: str) -> None:
|
|
146
|
+
"""Convert a .pptx file to .docx: each slide's title becomes a heading, its text becomes
|
|
147
|
+
paragraphs, and any picture on the slide is inserted into the document."""
|
|
148
|
+
from docx import Document
|
|
149
|
+
prs = Presentation(path)
|
|
150
|
+
doc = Document()
|
|
151
|
+
for i, slide in enumerate(prs.slides):
|
|
152
|
+
texts = []
|
|
153
|
+
image_blobs = []
|
|
154
|
+
for shape in slide.shapes:
|
|
155
|
+
if shape.has_text_frame:
|
|
156
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
157
|
+
line = "".join(run.text for run in paragraph.runs)
|
|
158
|
+
if line:
|
|
159
|
+
texts.append(line)
|
|
160
|
+
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
|
161
|
+
image_blobs.append(shape.image.blob)
|
|
162
|
+
if not texts and not image_blobs:
|
|
163
|
+
continue
|
|
164
|
+
if texts:
|
|
165
|
+
doc.add_heading(texts[0], level=1)
|
|
166
|
+
for line in texts[1:]:
|
|
167
|
+
doc.add_paragraph(line)
|
|
168
|
+
for blob in image_blobs:
|
|
169
|
+
doc.add_picture(io.BytesIO(blob), width=Inches(4))
|
|
170
|
+
if i < len(prs.slides) - 1:
|
|
171
|
+
doc.add_page_break()
|
|
172
|
+
doc.save(output_path)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def docx_to_pptx(path: str, output_path: str, theme_name: str | None = None) -> None:
|
|
176
|
+
"""Convert a .docx file to a themed .pptx: headings (or bolded short lines) start new
|
|
177
|
+
slides, normal paragraphs become bullet content, and any embedded image is placed
|
|
178
|
+
on its corresponding slide. theme_name picks a specific theme; omit for a random one."""
|
|
179
|
+
from docx import Document
|
|
180
|
+
import tempfile
|
|
181
|
+
|
|
182
|
+
doc = Document(path)
|
|
183
|
+
theme = _pick_theme(theme_name)
|
|
184
|
+
prs = Presentation()
|
|
185
|
+
|
|
186
|
+
current_title = None
|
|
187
|
+
current_body: list[str] = []
|
|
188
|
+
current_image: str | None = None
|
|
189
|
+
|
|
190
|
+
def flush():
|
|
191
|
+
if current_title is not None:
|
|
192
|
+
_add_slide_with_text(prs, theme, current_title, current_body, current_image)
|
|
193
|
+
|
|
194
|
+
for p in doc.paragraphs:
|
|
195
|
+
text = p.text.strip()
|
|
196
|
+
images = _extract_paragraph_images(p, doc)
|
|
197
|
+
image_path = None
|
|
198
|
+
if images:
|
|
199
|
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
|
|
200
|
+
tmp.write(images[0])
|
|
201
|
+
tmp.close()
|
|
202
|
+
image_path = tmp.name
|
|
203
|
+
|
|
204
|
+
if not text and not image_path:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if text and _looks_like_heading(p):
|
|
208
|
+
flush()
|
|
209
|
+
current_title = text
|
|
210
|
+
current_body = []
|
|
211
|
+
current_image = image_path
|
|
212
|
+
else:
|
|
213
|
+
if current_title is None:
|
|
214
|
+
current_title = text or "Untitled"
|
|
215
|
+
elif text:
|
|
216
|
+
current_body.append(text)
|
|
217
|
+
if image_path and current_image is None:
|
|
218
|
+
current_image = image_path
|
|
219
|
+
flush()
|
|
220
|
+
|
|
221
|
+
prs.save(output_path)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def pdf_to_pptx(path: str, output_path: str, theme_name: str | None = None) -> None:
|
|
225
|
+
"""Convert a PDF to a themed .pptx: each page's extracted text and first embedded
|
|
226
|
+
image (if any) are placed onto their own slide. theme_name picks a specific theme;
|
|
227
|
+
omit for a random one."""
|
|
228
|
+
from pypdf import PdfReader
|
|
229
|
+
import tempfile
|
|
230
|
+
|
|
231
|
+
reader = PdfReader(path)
|
|
232
|
+
theme = _pick_theme(theme_name)
|
|
233
|
+
prs = Presentation()
|
|
234
|
+
|
|
235
|
+
for i, page in enumerate(reader.pages):
|
|
236
|
+
text = page.extract_text() or ""
|
|
237
|
+
lines = [l for l in text.split("\n") if l.strip()]
|
|
238
|
+
title = lines[0] if lines else f"Page {i + 1}"
|
|
239
|
+
body = lines[1:] if len(lines) > 1 else []
|
|
240
|
+
|
|
241
|
+
image_path = None
|
|
242
|
+
try:
|
|
243
|
+
images = page.images
|
|
244
|
+
if len(images) > 0:
|
|
245
|
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
|
|
246
|
+
images[0].image.save(tmp.name)
|
|
247
|
+
tmp.close()
|
|
248
|
+
image_path = tmp.name
|
|
249
|
+
except Exception:
|
|
250
|
+
image_path = None
|
|
251
|
+
|
|
252
|
+
_add_slide_with_text(prs, theme, title, body, image_path)
|
|
253
|
+
|
|
254
|
+
prs.save(output_path)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def extract_pptx_notes(path: str) -> list[str]:
|
|
258
|
+
"""Extract the speaker notes from every slide in a .pptx file."""
|
|
259
|
+
prs = Presentation(path)
|
|
260
|
+
notes = []
|
|
261
|
+
for slide in prs.slides:
|
|
262
|
+
if slide.has_notes_slide:
|
|
263
|
+
notes.append(slide.notes_slide.notes_text_frame.text)
|
|
264
|
+
else:
|
|
265
|
+
notes.append("")
|
|
266
|
+
return notes
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def merge_pptx_files(paths: list[str], output_path: str, theme_name: str | None = None) -> None:
|
|
270
|
+
"""Merge multiple .pptx files' content into one themed deck, preserving slide order."""
|
|
271
|
+
theme = _pick_theme(theme_name)
|
|
272
|
+
prs = Presentation()
|
|
273
|
+
for path in paths:
|
|
274
|
+
source = Presentation(path)
|
|
275
|
+
for slide in source.slides:
|
|
276
|
+
texts = []
|
|
277
|
+
for shape in slide.shapes:
|
|
278
|
+
if shape.has_text_frame:
|
|
279
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
280
|
+
line = "".join(run.text for run in paragraph.runs)
|
|
281
|
+
if line:
|
|
282
|
+
texts.append(line)
|
|
283
|
+
title = texts[0] if texts else "Untitled"
|
|
284
|
+
body = texts[1:] if len(texts) > 1 else []
|
|
285
|
+
_add_slide_with_text(prs, theme, title, body)
|
|
286
|
+
prs.save(output_path)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quicktools-atom
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
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
|
|
@@ -20,13 +20,16 @@ Requires-Dist: python-pptx
|
|
|
20
20
|
Requires-Dist: faster-whisper
|
|
21
21
|
Requires-Dist: av
|
|
22
22
|
Requires-Dist: pyannote.audio
|
|
23
|
+
Requires-Dist: torch
|
|
24
|
+
Provides-Extra: diarization
|
|
25
|
+
Requires-Dist: pyannote.audio; extra == "diarization"
|
|
26
|
+
Requires-Dist: torch; extra == "diarization"
|
|
23
27
|
|
|
24
28
|
"""
|
|
25
29
|
|
|
26
30
|
<p align="center">
|
|
27
31
|
|
|
28
|
-
 
|
|
29
|
-
|
|
32
|
+
  
|
|
30
33
|
</p>
|
|
31
34
|
|
|
32
35
|
|
|
@@ -51,8 +54,7 @@ under \*\*AtomDev Studios\*\*.
|
|
|
51
54
|
|
|
52
55
|
<p align="center">
|
|
53
56
|
|
|
54
|
-
 
|
|
55
|
-
|
|
57
|
+
  
|
|
56
58
|
</p>
|
|
57
59
|
|
|
58
60
|
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools_atom.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{quicktools_atom-0.7.0 → quicktools_atom-0.7.2}/src/quicktools_atom.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|