oto-cli 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.
- oto/__init__.py +3 -0
- oto/cli.py +74 -0
- oto/commands/__init__.py +0 -0
- oto/commands/anthropic.py +64 -0
- oto/commands/audio.py +91 -0
- oto/commands/browser.py +257 -0
- oto/commands/company.py +23 -0
- oto/commands/enrichment.py +152 -0
- oto/commands/folk.py +238 -0
- oto/commands/google.py +355 -0
- oto/commands/notion.py +58 -0
- oto/commands/pennylane.py +272 -0
- oto/commands/search.py +36 -0
- oto/commands/sirene.py +201 -0
- oto/commands/skills.py +91 -0
- oto/commands/whatsapp.py +47 -0
- oto/config.py +167 -0
- oto/tools/.cache/notion/07a366cf2d7a0f77e7a1736805e94786e6053bb581b6b7179591b71d34843ce8.json +98 -0
- oto/tools/.cache/notion/0b21878dbd4bf5a67ff52104361a07ee575b03a075b7b81cbc4a0d5d4045f759.json +142 -0
- oto/tools/.cache/notion/0bf47e753ec29da03776bca010122393b4fc188af7f28dd11b456a74e5cae8f4.json +98 -0
- oto/tools/.cache/notion/1b2c924688fdcef8b7e3748f92ed4fdb232cadb387a7e9e157c438d32374ff97.json +4356 -0
- oto/tools/.cache/notion/2a66512043d3531dc900b8244fc13972c0abfe7760c10fbb7fc21f309238ad0b.json +132 -0
- oto/tools/.cache/notion/2b59aed15d0c747ec2cae70a1eb3f554b3e170875110b82f8e4614b761c84dcf.json +142 -0
- oto/tools/.cache/notion/2f7d1284021d30e842cc9a2b506e87b4a32c61e8e9abb616aec37828c441231d.json +220 -0
- oto/tools/.cache/notion/33eb548bce70fb7b0e8eb09211ba6c03ad22b6e7a289606b05c5d690f724d8e4.json +1659 -0
- oto/tools/.cache/notion/364d3a5e790f3d8b5137aae0a5a9ee151cd8c9f7dacb4fd669220f968405ea36.json +88 -0
- oto/tools/.cache/notion/3e7e836a6bf18f951d4887fefeacb146473491ad2cbaa1793c79af08b7ef45b6.json +132 -0
- oto/tools/.cache/notion/3f95b2d8bdf3d47e8f6c167d6d9a0889f27b64e8c78f0afdad538fecae070858.json +98 -0
- oto/tools/.cache/notion/46c38d10065fb68ca912692df53b6e54eb1f6f9cdff342de563020917e27ccf5.json +98 -0
- oto/tools/.cache/notion/51fa4d44044d751ca2319d87d607bd58a01004a24a8065808d65c273b35d3a10.json +210 -0
- oto/tools/.cache/notion/59888e9c297cda6836afb560fb4048b100f98eb3abead8b175fe92fbf858210b.json +186 -0
- oto/tools/.cache/notion/60df8d17a4e08fdbafb0ea398ff68d45cb79da7c01bd129e98264be1bff7ed1d.json +176 -0
- oto/tools/.cache/notion/62a44af3f86b5841d905789565a0208f80c1a304bcfc8889ccd55f61f29508b2.json +132 -0
- oto/tools/.cache/notion/69ec094fc12a40d1591abbd4f21295af672389963f900ea421a6d75b6f21f03b.json +176 -0
- oto/tools/.cache/notion/703fc49f07e73447e176fa69f31c7ff7bbb7c8c95325af4ca1a8664385bfeb0b.json +98 -0
- oto/tools/.cache/notion/7e247cf818c2a930de8a5a8599716900cb5af90570a9f701699dd8897fe43c8d.json +88 -0
- oto/tools/.cache/notion/86741d020ca89789bda8156315ead5e980068c19754baf6e611ffc618557dc06.json +115 -0
- oto/tools/.cache/notion/8cc6e5e1d955310412df09c9179ec67d42767d1b557262e28bc2afea05ab7c24.json +210 -0
- oto/tools/.cache/notion/989d88c22dd009e5683987e70671d1e20fd6a5a3e9ee7670db9ea602fe8fbded.json +4605 -0
- oto/tools/.cache/notion/999d01d80c8ef0c856572028e31e701cad8c5704b8886388b756d32ef50cbfaf.json +132 -0
- oto/tools/.cache/notion/9f10e5fa6404211b156b2f5c22a0644e5eb0c49774d2b4f8d768211f0c6aea22.json +3380 -0
- oto/tools/.cache/notion/9f950bdfc2aeb9ae5e1b185e827636f83fa44a9d7de0d1462d87926508b26907.json +142 -0
- oto/tools/.cache/notion/a4719c76c9ff4c4b056e87689e2f487770c35d82d70acb098ead7712e0d3be8d.json +142 -0
- oto/tools/.cache/notion/a808160a7352fa1a3029ef1e2929dfa99b5021b0d22ce1cfb9efe3c68f7e7af1.json +98 -0
- oto/tools/.cache/notion/a9c29806556409fbfcd7622b876244caed085c63c70380dc6caacd162e6d05a9.json +132 -0
- oto/tools/.cache/notion/b3142c5509452a15382cade5cb81324a26d1946d128a1b4342a143872f9354fe.json +149 -0
- oto/tools/.cache/notion/b516a0820a66ae04229bfcb7f0463be0867f31a8dc1b875e57aba02b970a7a31.json +98 -0
- oto/tools/.cache/notion/c175f8c35c81ebd2fb9e4bf4d2f1cc29f98b45006f59354c9f33ee780609d896.json +247 -0
- oto/tools/.cache/notion/c950254390b901d475937c868e27e74960720fe65e2d8506bd39e30c634e619d.json +98 -0
- oto/tools/.cache/notion/cac99fb54fa178e1a7f72e5212e1e99eff5fe72a1fa52f65fcee432bcf528b53.json +166 -0
- oto/tools/.cache/notion/e38237abecf19ce5b9a92ed90723d2642a11b0f1ef189b1a1fc10b7ebcd3d4a4.json +88 -0
- oto/tools/.cache/notion/e58e933def9b7000d791c83f6660adbecac59ab61e54f8d855a52d80200eb929.json +292 -0
- oto/tools/.cache/notion/f8088377a1049850b0ec1acabf09da8504fb1f194479cc43ce4c31db3895afcd.json +176 -0
- oto/tools/.cache/notion/fc2ccb6edbe2020010e036a69e7ece3ab3f007c9a4fed39066d6792d332e65af.json +142 -0
- oto/tools/.cache/notion/ff117c74176f8fc26aba7f06649c7794dc4a0c51194033f453b16c8af8ac4201.json +36 -0
- oto/tools/__init__.py +1 -0
- oto/tools/anthropic/__init__.py +5 -0
- oto/tools/anthropic/client.py +354 -0
- oto/tools/anthropic_batch/__init__.py +5 -0
- oto/tools/anthropic_batch/client.py +352 -0
- oto/tools/apollo/__init__.py +5 -0
- oto/tools/apollo/client.py +191 -0
- oto/tools/attio/__init__.py +5 -0
- oto/tools/attio/client.py +242 -0
- oto/tools/audio/__init__.py +5 -0
- oto/tools/audio/client.py +116 -0
- oto/tools/browser/__init__.py +26 -0
- oto/tools/browser/crunchbase.py +423 -0
- oto/tools/browser/g2.py +236 -0
- oto/tools/browser/indeed.py +282 -0
- oto/tools/browser/linkedin.py +821 -0
- oto/tools/browser/pappers.py +344 -0
- oto/tools/clearbit/__init__.py +5 -0
- oto/tools/clearbit/client.py +126 -0
- oto/tools/collective/__init__.py +5 -0
- oto/tools/collective/client.py +333 -0
- oto/tools/common/__init__.py +5 -0
- oto/tools/common/rate_limiter.py +457 -0
- oto/tools/figma/__init__.py +5 -0
- oto/tools/figma/client.py +254 -0
- oto/tools/folk/__init__.py +5 -0
- oto/tools/folk/client.py +203 -0
- oto/tools/gemini/__init__.py +5 -0
- oto/tools/gemini/client.py +264 -0
- oto/tools/google/__init__.py +1 -0
- oto/tools/google/calendar/__init__.py +0 -0
- oto/tools/google/calendar/lib/__init__.py +0 -0
- oto/tools/google/calendar/lib/calendar_client.py +155 -0
- oto/tools/google/credentials.py +177 -0
- oto/tools/google/docs/get_section.py +53 -0
- oto/tools/google/docs/insert_section.py +56 -0
- oto/tools/google/docs/insert_text.py +99 -0
- oto/tools/google/docs/lib/__init__.py +0 -0
- oto/tools/google/docs/lib/docs_client.py +341 -0
- oto/tools/google/docs/list_headings.py +44 -0
- oto/tools/google/docs/move_section.py +37 -0
- oto/tools/google/docs/replace_section.py +47 -0
- oto/tools/google/docs/sync/__init__.py +1 -0
- oto/tools/google/docs/sync/gdoc_sync.py +427 -0
- oto/tools/google/drive/README.md +308 -0
- oto/tools/google/drive/check_quota.py +31 -0
- oto/tools/google/drive/copy_file.py +76 -0
- oto/tools/google/drive/create_folder.py +58 -0
- oto/tools/google/drive/download_file.py +60 -0
- oto/tools/google/drive/export_doc.py +64 -0
- oto/tools/google/drive/export_slides.py +95 -0
- oto/tools/google/drive/extract_slides_content.py +113 -0
- oto/tools/google/drive/lib/drive_client.py +454 -0
- oto/tools/google/drive/list_files.py +97 -0
- oto/tools/google/drive/list_shared_drives.py +32 -0
- oto/tools/google/drive/move_file.py +57 -0
- oto/tools/google/drive/requirements.txt +4 -0
- oto/tools/google/drive/tool.yaml +210 -0
- oto/tools/google/drive/upload_file.py +90 -0
- oto/tools/google/gmail/__init__.py +1 -0
- oto/tools/google/gmail/get_message.py +33 -0
- oto/tools/google/gmail/lib/__init__.py +1 -0
- oto/tools/google/gmail/lib/gmail_client.py +385 -0
- oto/tools/google/gmail/list_messages.py +37 -0
- oto/tools/google/gmail/requirements.txt +3 -0
- oto/tools/google/gmail/search.py +34 -0
- oto/tools/google/gmail/send.py +39 -0
- oto/tools/google/gmail/tool.yaml +107 -0
- oto/tools/google/keep/__init__.py +0 -0
- oto/tools/google/keep/create_note.py +42 -0
- oto/tools/google/keep/get_master_token.py +101 -0
- oto/tools/google/keep/get_note.py +33 -0
- oto/tools/google/keep/lib/__init__.py +0 -0
- oto/tools/google/keep/lib/keep_client.py +273 -0
- oto/tools/google/keep/list_notes.py +42 -0
- oto/tools/google/keep/search.py +34 -0
- oto/tools/google/sheets/create_sheet_direct.py +157 -0
- oto/tools/google/sheets/create_sheet_from_csv.py +167 -0
- oto/tools/google/sheets/create_sheet_in_folder.py +165 -0
- oto/tools/google/sheets/create_sheet_via_drive.py +157 -0
- oto/tools/google/sheets/tool.yaml +74 -0
- oto/tools/google/slides/.folders +19 -0
- oto/tools/google/slides/321-LAYOUTS.md +203 -0
- oto/tools/google/slides/README.md +210 -0
- oto/tools/google/slides/USAGE-EDIT.md +626 -0
- oto/tools/google/slides/USAGE.md +158 -0
- oto/tools/google/slides/create-demo-presentation.py +348 -0
- oto/tools/google/slides/generate_slides.py +437 -0
- oto/tools/google/slides/lib/__init__.py +1 -0
- oto/tools/google/slides/lib/content_filler.py +579 -0
- oto/tools/google/slides/lib/layout_mappings.py +182 -0
- oto/tools/google/slides/lib/slides_client.py +1239 -0
- oto/tools/google/slides/requirements.txt +5 -0
- oto/tools/google/slides/test-copy-and-edit.py +187 -0
- oto/tools/google/slides/test-copy-slide.py +148 -0
- oto/tools/google/slides/test-edit.py +183 -0
- oto/tools/google/slides/tool.yaml +37 -0
- oto/tools/groq/__init__.py +5 -0
- oto/tools/groq/client.py +165 -0
- oto/tools/hithorizons/__init__.py +5 -0
- oto/tools/hithorizons/client.py +168 -0
- oto/tools/hunter/__init__.py +5 -0
- oto/tools/hunter/client.py +104 -0
- oto/tools/kaspr/__init__.py +5 -0
- oto/tools/kaspr/client.py +80 -0
- oto/tools/lemlist/__init__.py +5 -0
- oto/tools/lemlist/client.py +486 -0
- oto/tools/mistral/__init__.py +5 -0
- oto/tools/mistral/client.py +149 -0
- oto/tools/naf/__init__.py +5 -0
- oto/tools/naf/suggester.py +140 -0
- oto/tools/notion/README.md +310 -0
- oto/tools/notion/append_blocks.py +204 -0
- oto/tools/notion/append_blocks_chunked.py +296 -0
- oto/tools/notion/count_database_entries.py +47 -0
- oto/tools/notion/create_database_from_csv.py +269 -0
- oto/tools/notion/create_page.py +65 -0
- oto/tools/notion/get_database.py +67 -0
- oto/tools/notion/get_page.py +78 -0
- oto/tools/notion/known-pages.md +75 -0
- oto/tools/notion/lib/markdown_converter.py +287 -0
- oto/tools/notion/lib/notion_client.py +313 -0
- oto/tools/notion/list_teamspaces.py +244 -0
- oto/tools/notion/notion +122 -0
- oto/tools/notion/query_database.py +77 -0
- oto/tools/notion/requirements.txt +1 -0
- oto/tools/notion/search.py +93 -0
- oto/tools/notion/tool.yaml +235 -0
- oto/tools/notion/update_page.py +65 -0
- oto/tools/pennylane/__init__.py +5 -0
- oto/tools/pennylane/client.py +386 -0
- oto/tools/phantombuster/__init__.py +5 -0
- oto/tools/phantombuster/client.py +177 -0
- oto/tools/resend/__init__.py +5 -0
- oto/tools/resend/client.py +149 -0
- oto/tools/serpapi/__init__.py +5 -0
- oto/tools/serpapi/client.py +128 -0
- oto/tools/serper/__init__.py +5 -0
- oto/tools/serper/client.py +210 -0
- oto/tools/sirene/__init__.py +34 -0
- oto/tools/sirene/client.py +474 -0
- oto/tools/sirene/data/naf_codes.txt +732 -0
- oto/tools/sirene/entreprises.py +175 -0
- oto/tools/sirene/stock.py +377 -0
- oto/tools/slack/__init__.py +5 -0
- oto/tools/slack/client.py +202 -0
- oto/tools/unsplash/__init__.py +5 -0
- oto/tools/unsplash/client.py +197 -0
- oto/tools/whatsapp/__init__.py +3 -0
- oto/tools/whatsapp/client.py +68 -0
- oto/tools/whatsapp/node/package-lock.json +1564 -0
- oto/tools/whatsapp/node/package.json +11 -0
- oto/tools/whatsapp/node/whatsapp.mjs +357 -0
- oto/tools/wttj/__init__.py +5 -0
- oto/tools/wttj/client.py +251 -0
- oto/tools/zerobounce/__init__.py +5 -0
- oto/tools/zerobounce/client.py +93 -0
- oto_cli-1.0.0.dist-info/METADATA +171 -0
- oto_cli-1.0.0.dist-info/RECORD +217 -0
- oto_cli-1.0.0.dist-info/WHEEL +4 -0
- oto_cli-1.0.0.dist-info/entry_points.txt +2 -0
- oto_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Generate Google Slides presentation from .slides.yaml file
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 generate_slides.py --input presentation.slides.yaml [--share]
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import yaml
|
|
12
|
+
import typer
|
|
13
|
+
from typing_extensions import Annotated
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Add parent directory to path for imports
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
19
|
+
from lib.slides_client import SlidesClient
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(help="Generate Google Slides from YAML")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_credentials_path():
|
|
25
|
+
"""Load Google service account credentials path from local .keys directory."""
|
|
26
|
+
credentials_path = Path(__file__).parent / '.keys' / 'gdrive-key.json'
|
|
27
|
+
|
|
28
|
+
if not credentials_path.exists():
|
|
29
|
+
raise FileNotFoundError(f"Credentials file not found at {credentials_path}")
|
|
30
|
+
|
|
31
|
+
return str(credentials_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_default_folder():
|
|
35
|
+
"""Load default folder ID from .folders file."""
|
|
36
|
+
folders_path = Path(__file__).parent / '.folders'
|
|
37
|
+
|
|
38
|
+
if not folders_path.exists():
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
with open(folders_path, 'r') as f:
|
|
42
|
+
for line in f:
|
|
43
|
+
line = line.strip()
|
|
44
|
+
if line.startswith('DEFAULT_FOLDER='):
|
|
45
|
+
return line.split('=', 1)[1].strip()
|
|
46
|
+
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_default_template():
|
|
51
|
+
"""Load default template ID from .folders file."""
|
|
52
|
+
folders_path = Path(__file__).parent / '.folders'
|
|
53
|
+
|
|
54
|
+
if not folders_path.exists():
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
with open(folders_path, 'r') as f:
|
|
58
|
+
for line in f:
|
|
59
|
+
line = line.strip()
|
|
60
|
+
if line.startswith('DEFAULT_TEMPLATE='):
|
|
61
|
+
return line.split('=', 1)[1].strip()
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_slides_yaml(file_path):
|
|
67
|
+
"""Load and parse slides YAML file"""
|
|
68
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
69
|
+
return yaml.safe_load(f)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def extract_images_from_markdown(content):
|
|
73
|
+
"""
|
|
74
|
+
Extract image references from markdown content
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
list: List of dicts with 'alt' (alt text) and 'url' (image URL/path)
|
|
78
|
+
"""
|
|
79
|
+
if not content:
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
import re
|
|
83
|
+
# Match markdown images: 
|
|
84
|
+
pattern = r'!\[([^\]]*)\]\(([^\)]+)\)'
|
|
85
|
+
matches = re.findall(pattern, content)
|
|
86
|
+
|
|
87
|
+
return [{'alt': alt, 'url': url} for alt, url in matches]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_google_layout_for_yaml_layout(yaml_layout):
|
|
91
|
+
"""
|
|
92
|
+
Map YAML layout to Google Slides layout
|
|
93
|
+
|
|
94
|
+
Uses the complete 321 template layout mappings
|
|
95
|
+
"""
|
|
96
|
+
from lib.layout_mappings import get_api_name
|
|
97
|
+
return get_api_name(yaml_layout)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def generate_presentation(yaml_path, share=False, folder_id=None, template_id=None, output_format='url'):
|
|
103
|
+
"""
|
|
104
|
+
Generate Google Slides presentation from YAML
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
yaml_path: Path to .slides.yaml file
|
|
108
|
+
share: Make presentation publicly accessible
|
|
109
|
+
folder_id: Optional Google Drive folder ID
|
|
110
|
+
template_id: Optional presentation ID to use as template (for theme)
|
|
111
|
+
output_format: 'url' or 'json'
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
dict: Presentation info (id, url, title)
|
|
115
|
+
"""
|
|
116
|
+
# Load YAML
|
|
117
|
+
data = load_slides_yaml(yaml_path)
|
|
118
|
+
|
|
119
|
+
# Initialize client with credentials from .env.keys
|
|
120
|
+
credentials_path = load_credentials_path()
|
|
121
|
+
client = SlidesClient(credentials_path)
|
|
122
|
+
|
|
123
|
+
# Use folder_id or default from .folders
|
|
124
|
+
if folder_id is None:
|
|
125
|
+
folder_id = load_default_folder()
|
|
126
|
+
|
|
127
|
+
# Get template_id from YAML, command line, or default (in order of priority)
|
|
128
|
+
if template_id is None:
|
|
129
|
+
template_id = data.get('template_id')
|
|
130
|
+
if template_id is None:
|
|
131
|
+
template_id = load_default_template()
|
|
132
|
+
|
|
133
|
+
# Create a parent folder for this deck (deck + images)
|
|
134
|
+
title = data.get('title', 'Untitled Presentation')
|
|
135
|
+
deck_folder_name = title
|
|
136
|
+
print(f"Creating deck folder: {deck_folder_name}")
|
|
137
|
+
deck_folder_id = client.create_folder(deck_folder_name, parent_folder_id=folder_id)
|
|
138
|
+
|
|
139
|
+
# Create presentation in the deck folder
|
|
140
|
+
presentation = client.create_presentation(title, folder_id=deck_folder_id, template_id=template_id)
|
|
141
|
+
presentation_id = presentation['presentationId']
|
|
142
|
+
|
|
143
|
+
# Remove all slides from template (if template was used)
|
|
144
|
+
if template_id:
|
|
145
|
+
pres = client.get_presentation(presentation_id)
|
|
146
|
+
existing_slides = pres.get('slides', [])
|
|
147
|
+
|
|
148
|
+
if existing_slides:
|
|
149
|
+
# Delete all slides in one batch request
|
|
150
|
+
delete_requests = [
|
|
151
|
+
{'deleteObject': {'objectId': slide['objectId']}}
|
|
152
|
+
for slide in existing_slides
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
client.slides_service.presentations().batchUpdate(
|
|
156
|
+
presentationId=presentation_id,
|
|
157
|
+
body={'requests': delete_requests}
|
|
158
|
+
).execute()
|
|
159
|
+
else:
|
|
160
|
+
# Remove default slide for new presentations
|
|
161
|
+
pres = client.get_presentation(presentation_id)
|
|
162
|
+
if pres.get('slides'):
|
|
163
|
+
first_slide_id = pres['slides'][0]['objectId']
|
|
164
|
+
client.slides_service.presentations().batchUpdate(
|
|
165
|
+
presentationId=presentation_id,
|
|
166
|
+
body={'requests': [{'deleteObject': {'objectId': first_slide_id}}]}
|
|
167
|
+
).execute()
|
|
168
|
+
|
|
169
|
+
# Get base path for resolving relative image paths
|
|
170
|
+
yaml_base_path = Path(yaml_path).parent
|
|
171
|
+
|
|
172
|
+
# Create an "images" subfolder in the deck folder if we have any images
|
|
173
|
+
images_folder_id = None
|
|
174
|
+
has_images = any(
|
|
175
|
+
extract_images_from_markdown(slide.get('content', '')) or
|
|
176
|
+
extract_images_from_markdown('\n\n'.join([col.get('content', '') for col in slide.get('columns', [])]))
|
|
177
|
+
for slide in data.get('slides', [])
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if has_images:
|
|
181
|
+
print(f"Creating images subfolder in {deck_folder_name}/")
|
|
182
|
+
images_folder_id = client.create_folder("images", parent_folder_id=deck_folder_id)
|
|
183
|
+
|
|
184
|
+
# Create content filler
|
|
185
|
+
from lib.content_filler import ContentFiller
|
|
186
|
+
filler = ContentFiller(client, presentation_id)
|
|
187
|
+
|
|
188
|
+
# Add slides
|
|
189
|
+
slides = data.get('slides', [])
|
|
190
|
+
for slide_data in slides:
|
|
191
|
+
# Map YAML layout to Google Slides layout
|
|
192
|
+
yaml_layout = slide_data.get('layout', 'default')
|
|
193
|
+
google_layout = get_google_layout_for_yaml_layout(yaml_layout)
|
|
194
|
+
|
|
195
|
+
# Create slide with native Google layout
|
|
196
|
+
slide_id = client.add_slide(presentation_id, layout=google_layout)
|
|
197
|
+
|
|
198
|
+
# Fill content using smart strategy-based filler
|
|
199
|
+
filler.fill_slide(slide_id, yaml_layout, slide_data)
|
|
200
|
+
|
|
201
|
+
# Handle single image field (for layouts with image placeholders)
|
|
202
|
+
single_image = slide_data.get('image')
|
|
203
|
+
if single_image:
|
|
204
|
+
# Get image placeholders in this slide
|
|
205
|
+
image_placeholders = client.get_image_placeholders_in_slide(presentation_id, slide_id)
|
|
206
|
+
|
|
207
|
+
if image_placeholders:
|
|
208
|
+
image_url = single_image
|
|
209
|
+
|
|
210
|
+
# Resolve local paths
|
|
211
|
+
if not image_url.startswith(('http://', 'https://')):
|
|
212
|
+
if image_url.startswith('/api/media/'):
|
|
213
|
+
import re
|
|
214
|
+
match = re.search(r'/api/media/runs/([^/]+)/(.+)', image_url)
|
|
215
|
+
if match:
|
|
216
|
+
run_id, file_path = match.groups()
|
|
217
|
+
project_root = yaml_base_path
|
|
218
|
+
while project_root.name != '' and not (project_root / 'agents').exists():
|
|
219
|
+
project_root = project_root.parent
|
|
220
|
+
local_path = project_root / 'agents' / run_id / file_path
|
|
221
|
+
else:
|
|
222
|
+
local_path = yaml_base_path / image_url.lstrip('/')
|
|
223
|
+
else:
|
|
224
|
+
local_path = yaml_base_path / image_url
|
|
225
|
+
|
|
226
|
+
# Upload to Drive
|
|
227
|
+
if local_path.exists():
|
|
228
|
+
print(f"Uploading image for placeholder: {local_path}")
|
|
229
|
+
upload_folder = images_folder_id if images_folder_id else folder_id
|
|
230
|
+
image_url = client.upload_image_to_drive(str(local_path), upload_folder)
|
|
231
|
+
else:
|
|
232
|
+
print(f"Warning: Image not found: {local_path}")
|
|
233
|
+
image_url = None
|
|
234
|
+
|
|
235
|
+
# Replace first image placeholder
|
|
236
|
+
if image_url:
|
|
237
|
+
try:
|
|
238
|
+
client.replace_image_placeholder(
|
|
239
|
+
presentation_id,
|
|
240
|
+
image_placeholders[0], # Use first placeholder
|
|
241
|
+
image_url,
|
|
242
|
+
replace_method='CENTER_INSIDE'
|
|
243
|
+
)
|
|
244
|
+
print(f"Replaced image placeholder with: {single_image}")
|
|
245
|
+
except Exception as e:
|
|
246
|
+
print(f"Error replacing image placeholder: {e}")
|
|
247
|
+
else:
|
|
248
|
+
print(f"Warning: 'image' field specified but layout has no image placeholder")
|
|
249
|
+
|
|
250
|
+
# Extract images for insertion
|
|
251
|
+
columns = slide_data.get('columns', [])
|
|
252
|
+
|
|
253
|
+
# Get slide dimensions (standard 16:9 - 10" x 5.625")
|
|
254
|
+
# 1 inch = 914400 EMU
|
|
255
|
+
slide_width = 10 * 914400
|
|
256
|
+
slide_height = 5.625 * 914400
|
|
257
|
+
|
|
258
|
+
# Handle column layouts: extract images per column
|
|
259
|
+
if columns and yaml_layout in ['2-columns', '3-columns']:
|
|
260
|
+
# Extract images per column
|
|
261
|
+
images_by_column = []
|
|
262
|
+
for col in columns:
|
|
263
|
+
col_images = extract_images_from_markdown(col.get('content', ''))
|
|
264
|
+
images_by_column.append(col_images)
|
|
265
|
+
|
|
266
|
+
# Position images at the top of each column
|
|
267
|
+
num_columns = len(columns)
|
|
268
|
+
column_width = slide_width / num_columns
|
|
269
|
+
margin = 0.5 * 914400 # 0.5 inch margin
|
|
270
|
+
|
|
271
|
+
# Smaller images for column layouts
|
|
272
|
+
img_width = column_width - (2 * margin) # Fit within column
|
|
273
|
+
img_height = img_width * 0.75 # Maintain 4:3 ratio
|
|
274
|
+
|
|
275
|
+
# Position: top of each column
|
|
276
|
+
y_top = 1.5 * 914400 # 1.5 inches from top (below title)
|
|
277
|
+
|
|
278
|
+
for col_idx, col_images in enumerate(images_by_column):
|
|
279
|
+
if not col_images:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
# X position for this column
|
|
283
|
+
x = (col_idx * column_width) + margin
|
|
284
|
+
|
|
285
|
+
# Insert first image of column (if multiple, only use first)
|
|
286
|
+
for img_data in col_images[:1]: # Only first image per column
|
|
287
|
+
image_url = img_data['url']
|
|
288
|
+
|
|
289
|
+
# Resolve local paths
|
|
290
|
+
if not image_url.startswith(('http://', 'https://')):
|
|
291
|
+
# Handle relative paths and /api/media paths
|
|
292
|
+
if image_url.startswith('/api/media/'):
|
|
293
|
+
# Convert /api/media/runs/xxx to local path
|
|
294
|
+
import re
|
|
295
|
+
match = re.search(r'/api/media/runs/([^/]+)/(.+)', image_url)
|
|
296
|
+
if match:
|
|
297
|
+
run_id, file_path = match.groups()
|
|
298
|
+
# Find the project root (where agents/ folder is)
|
|
299
|
+
project_root = yaml_base_path
|
|
300
|
+
while project_root.name != '' and not (project_root / 'agents').exists():
|
|
301
|
+
project_root = project_root.parent
|
|
302
|
+
local_path = project_root / 'agents' / run_id / file_path
|
|
303
|
+
else:
|
|
304
|
+
local_path = yaml_base_path / image_url.lstrip('/')
|
|
305
|
+
else:
|
|
306
|
+
local_path = yaml_base_path / image_url
|
|
307
|
+
|
|
308
|
+
# Upload to Drive and get public URL
|
|
309
|
+
if local_path.exists():
|
|
310
|
+
print(f"Uploading image: {local_path} (column {col_idx + 1})")
|
|
311
|
+
upload_folder = images_folder_id if images_folder_id else folder_id
|
|
312
|
+
image_url = client.upload_image_to_drive(str(local_path), upload_folder)
|
|
313
|
+
else:
|
|
314
|
+
print(f"Warning: Image not found: {local_path}")
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
# Insert image at top of column
|
|
318
|
+
try:
|
|
319
|
+
client.insert_image(presentation_id, slide_id, image_url,
|
|
320
|
+
int(x), int(y_top), int(img_width), int(img_height))
|
|
321
|
+
print(f"Inserted image in column {col_idx + 1}: {img_data['alt'] or 'Image'}")
|
|
322
|
+
except Exception as e:
|
|
323
|
+
print(f"Error inserting image: {e}")
|
|
324
|
+
|
|
325
|
+
else:
|
|
326
|
+
# Standard layouts: extract all images and center them
|
|
327
|
+
all_content = slide_data.get('content', '')
|
|
328
|
+
images = extract_images_from_markdown(all_content)
|
|
329
|
+
|
|
330
|
+
if images:
|
|
331
|
+
# Default image dimensions for standard layouts
|
|
332
|
+
img_width = 2.5 * 914400 # 2.5 inches (smaller than before)
|
|
333
|
+
img_height = img_width * 0.75 # Maintain 4:3 ratio
|
|
334
|
+
spacing = 0.5 * 914400
|
|
335
|
+
|
|
336
|
+
total_images = len(images)
|
|
337
|
+
|
|
338
|
+
for idx, img_data in enumerate(images):
|
|
339
|
+
image_url = img_data['url']
|
|
340
|
+
|
|
341
|
+
# Resolve local paths
|
|
342
|
+
if not image_url.startswith(('http://', 'https://')):
|
|
343
|
+
if image_url.startswith('/api/media/'):
|
|
344
|
+
import re
|
|
345
|
+
match = re.search(r'/api/media/runs/([^/]+)/(.+)', image_url)
|
|
346
|
+
if match:
|
|
347
|
+
run_id, file_path = match.groups()
|
|
348
|
+
project_root = yaml_base_path
|
|
349
|
+
while project_root.name != '' and not (project_root / 'agents').exists():
|
|
350
|
+
project_root = project_root.parent
|
|
351
|
+
local_path = project_root / 'agents' / run_id / file_path
|
|
352
|
+
else:
|
|
353
|
+
local_path = yaml_base_path / image_url.lstrip('/')
|
|
354
|
+
else:
|
|
355
|
+
local_path = yaml_base_path / image_url
|
|
356
|
+
|
|
357
|
+
if local_path.exists():
|
|
358
|
+
print(f"Uploading image: {local_path}")
|
|
359
|
+
upload_folder = images_folder_id if images_folder_id else folder_id
|
|
360
|
+
image_url = client.upload_image_to_drive(str(local_path), upload_folder)
|
|
361
|
+
else:
|
|
362
|
+
print(f"Warning: Image not found: {local_path}")
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
# Calculate position (distribute images horizontally)
|
|
366
|
+
if total_images == 1:
|
|
367
|
+
# Center single image
|
|
368
|
+
x = (slide_width - img_width) / 2
|
|
369
|
+
y = (slide_height - img_height) / 2 + (1 * 914400)
|
|
370
|
+
else:
|
|
371
|
+
# Arrange multiple images horizontally
|
|
372
|
+
total_width = (img_width * total_images) + (spacing * (total_images - 1))
|
|
373
|
+
start_x = (slide_width - total_width) / 2
|
|
374
|
+
x = start_x + (idx * (img_width + spacing))
|
|
375
|
+
y = (slide_height - img_height) / 2 + (1 * 914400)
|
|
376
|
+
|
|
377
|
+
# Insert image
|
|
378
|
+
try:
|
|
379
|
+
client.insert_image(presentation_id, slide_id, image_url,
|
|
380
|
+
int(x), int(y), int(img_width), int(img_height))
|
|
381
|
+
print(f"Inserted image: {img_data['alt'] or 'Image'}")
|
|
382
|
+
except Exception as e:
|
|
383
|
+
print(f"Error inserting image: {e}")
|
|
384
|
+
|
|
385
|
+
# Throttle API requests to avoid rate limiting (60 requests/minute limit)
|
|
386
|
+
# Sleep 2 seconds between slides to stay under limit
|
|
387
|
+
time.sleep(2)
|
|
388
|
+
|
|
389
|
+
# Share if requested
|
|
390
|
+
if share:
|
|
391
|
+
client.share_presentation(presentation_id)
|
|
392
|
+
|
|
393
|
+
# Get URL
|
|
394
|
+
url = client.get_presentation_url(presentation_id)
|
|
395
|
+
|
|
396
|
+
result = {
|
|
397
|
+
'id': presentation_id,
|
|
398
|
+
'title': title,
|
|
399
|
+
'url': url,
|
|
400
|
+
'slides_count': len(slides)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if output_format == 'json':
|
|
404
|
+
return json.dumps(result, indent=2)
|
|
405
|
+
else:
|
|
406
|
+
return url
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@app.command()
|
|
410
|
+
def main(
|
|
411
|
+
input: Annotated[str, typer.Option(help="Path to .slides.yaml file")],
|
|
412
|
+
share: Annotated[bool, typer.Option(help="Make presentation public")] = False,
|
|
413
|
+
folder_id: Annotated[Optional[str], typer.Option(help="Google Drive folder ID")] = None,
|
|
414
|
+
template_id: Annotated[Optional[str], typer.Option(help="Presentation ID to use as template (for theme)")] = None,
|
|
415
|
+
output: Annotated[str, typer.Option(help="Output format")] = 'url',
|
|
416
|
+
):
|
|
417
|
+
"""Generate Google Slides from YAML."""
|
|
418
|
+
if output not in ['url', 'json']:
|
|
419
|
+
print(f"Error: Invalid output format '{output}'. Choose from: url, json", file=sys.stderr)
|
|
420
|
+
raise typer.Exit(1)
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
result = generate_presentation(
|
|
424
|
+
input,
|
|
425
|
+
share=share,
|
|
426
|
+
folder_id=folder_id,
|
|
427
|
+
template_id=template_id,
|
|
428
|
+
output_format=output
|
|
429
|
+
)
|
|
430
|
+
print(result)
|
|
431
|
+
except Exception as e:
|
|
432
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
433
|
+
raise typer.Exit(1)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
if __name__ == '__main__':
|
|
437
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Google Slides tool library
|