@champpaba/gslide 0.1.3 → 0.1.4
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.
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/gslide/__init__.py +3 -2
- package/src/gslide/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/gslide/__pycache__/auth.cpython-313.pyc +0 -0
- package/src/gslide/__pycache__/browser.cpython-313.pyc +0 -0
- package/src/gslide/__pycache__/gen.cpython-313.pyc +0 -0
- package/src/gslide/__pycache__/prompts.cpython-313.pyc +0 -0
- package/src/gslide/auth.py +1 -9
- package/src/gslide/browser.py +1 -2
- package/src/gslide/gen.py +33 -54
- package/src/gslide/prompts.py +1 -3
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/gslide/__init__.py
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/gslide/auth.py
CHANGED
|
@@ -7,33 +7,25 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
SLIDES_URL = "https://docs.google.com/presentation/"
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def get_storage_path() -> Path:
|
|
15
|
-
"""Return the path to the storage state file."""
|
|
16
14
|
return Path.home() / ".gslide" / "storage_state.json"
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
def is_logged_in() -> bool:
|
|
20
|
-
"""Check if a storage state file exists."""
|
|
21
18
|
return get_storage_path().exists()
|
|
22
19
|
|
|
23
20
|
|
|
24
21
|
def save_storage_state(data: dict[str, Any]) -> None:
|
|
25
|
-
"""Save storage state data to disk, creating directories as needed."""
|
|
26
22
|
path = get_storage_path()
|
|
27
23
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
28
24
|
path.write_text(json.dumps(data, indent=2))
|
|
29
25
|
|
|
30
26
|
|
|
31
27
|
def delete_storage_state() -> None:
|
|
32
|
-
|
|
33
|
-
try:
|
|
34
|
-
get_storage_path().unlink()
|
|
35
|
-
except FileNotFoundError:
|
|
36
|
-
pass
|
|
28
|
+
get_storage_path().unlink(missing_ok=True)
|
|
37
29
|
|
|
38
30
|
|
|
39
31
|
def login() -> None:
|
package/src/gslide/browser.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from playwright.sync_api import
|
|
6
|
+
from playwright.sync_api import BrowserContext, Playwright, sync_playwright
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class BrowserSession:
|
|
@@ -32,7 +32,6 @@ class BrowserSession:
|
|
|
32
32
|
self._context.browser.close()
|
|
33
33
|
if self._pw:
|
|
34
34
|
self._pw.stop()
|
|
35
|
-
return None
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def save_session(context: BrowserContext, path: Path) -> None:
|
package/src/gslide/gen.py
CHANGED
|
@@ -20,7 +20,6 @@ from gslide.auth import get_storage_path
|
|
|
20
20
|
from gslide.browser import BrowserSession
|
|
21
21
|
from gslide.prompts import PromptsData
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
PRESENTATION_URL = "https://docs.google.com/presentation/d/{id}/edit"
|
|
25
24
|
DEFAULT_TIMEOUT_MS = 120_000 # 120s — generation can take 30-60s
|
|
26
25
|
PANEL_LOAD_TIMEOUT_MS = 10_000
|
|
@@ -53,8 +52,8 @@ def navigate_to_presentation(page: Page, presentation_id: str) -> None:
|
|
|
53
52
|
page.wait_for_selector(
|
|
54
53
|
'[aria-label="filmstrip"]', timeout=SLIDES_LOAD_TIMEOUT_MS
|
|
55
54
|
)
|
|
56
|
-
except PwTimeout:
|
|
57
|
-
raise GenerationError("Google Slides UI did not load in time")
|
|
55
|
+
except PwTimeout as e:
|
|
56
|
+
raise GenerationError("Google Slides UI did not load in time") from e
|
|
58
57
|
|
|
59
58
|
|
|
60
59
|
def open_panel(page: Page) -> None:
|
|
@@ -71,11 +70,7 @@ def open_panel(page: Page) -> None:
|
|
|
71
70
|
|
|
72
71
|
|
|
73
72
|
def _reopen_panel(page: Page, retries: int = 2) -> None:
|
|
74
|
-
"""Reopen the HMV panel after insert overlay is dismissed.
|
|
75
|
-
|
|
76
|
-
After inserting a slide, the panel may close. This clicks the HMV icon
|
|
77
|
-
and waits for tabs to reappear, with retry logic.
|
|
78
|
-
"""
|
|
73
|
+
"""Reopen the HMV panel after insert overlay is dismissed."""
|
|
79
74
|
for attempt in range(retries + 1):
|
|
80
75
|
try:
|
|
81
76
|
hmv = page.locator('div[aria-label="Help me visualize"]')
|
|
@@ -97,7 +92,6 @@ def select_tab(page: Page, tab_name: str) -> None:
|
|
|
97
92
|
|
|
98
93
|
|
|
99
94
|
def _find_visible_textarea(page: Page) -> object:
|
|
100
|
-
"""Find the first visible textarea in the panel."""
|
|
101
95
|
textareas = page.locator("textarea")
|
|
102
96
|
for i in range(textareas.count()):
|
|
103
97
|
ta = textareas.nth(i)
|
|
@@ -179,12 +173,7 @@ def fill_and_create(
|
|
|
179
173
|
|
|
180
174
|
|
|
181
175
|
def _click_preview_image(page: Page) -> None:
|
|
182
|
-
"""Click the generated preview image to open the insert overlay.
|
|
183
|
-
|
|
184
|
-
Validated flow: after generation, the preview appears as an img hosted on
|
|
185
|
-
googleusercontent.com. Clicking it opens a fullscreen preview with
|
|
186
|
-
"Insert on new slide" button.
|
|
187
|
-
"""
|
|
176
|
+
"""Click the generated preview image to open the insert overlay."""
|
|
188
177
|
preview = page.locator('img[src*="googleusercontent.com"]')
|
|
189
178
|
for i in range(preview.count()):
|
|
190
179
|
try:
|
|
@@ -203,46 +192,40 @@ def _click_preview_image(page: Page) -> None:
|
|
|
203
192
|
raise GenerationError("Generated preview image not found")
|
|
204
193
|
|
|
205
194
|
|
|
206
|
-
def
|
|
207
|
-
"""Click
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
btn = page.get_by_text("Insert on new slide")
|
|
195
|
+
def _click_insert_button(page: Page, text: str) -> None:
|
|
196
|
+
"""Click an insert button by text, raising GenerationError if not found."""
|
|
197
|
+
btn = page.get_by_text(text)
|
|
211
198
|
if btn.count() > 0 and btn.first.is_visible():
|
|
212
199
|
btn.first.click()
|
|
213
200
|
else:
|
|
214
|
-
raise GenerationError("'
|
|
201
|
+
raise GenerationError(f"'{text}' button not found after clicking preview")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def insert_infographic(page: Page) -> None:
|
|
205
|
+
"""Click preview then 'Insert on new slide' for infographic tab."""
|
|
206
|
+
_click_preview_image(page)
|
|
207
|
+
_click_insert_button(page, "Insert on new slide")
|
|
215
208
|
_wait_for_new_slide(page)
|
|
216
209
|
|
|
217
210
|
|
|
218
211
|
def insert_slide(page: Page) -> None:
|
|
219
|
-
"""Click preview
|
|
212
|
+
"""Click preview then 'Insert on new slide' for slide tab."""
|
|
220
213
|
_click_preview_image(page)
|
|
221
|
-
|
|
222
|
-
# Slide tab uses same "Insert on new slide" button after clicking preview
|
|
223
|
-
btn = page.get_by_text("Insert on new slide")
|
|
224
|
-
if btn.count() > 0 and btn.first.is_visible():
|
|
225
|
-
btn.first.click()
|
|
226
|
-
else:
|
|
227
|
-
raise GenerationError("'Insert on new slide' button not found for slide tab")
|
|
214
|
+
_click_insert_button(page, "Insert on new slide")
|
|
228
215
|
_wait_for_new_slide(page)
|
|
229
216
|
|
|
230
217
|
|
|
231
218
|
def insert_image(page: Page, insert_as: str = "image") -> None:
|
|
232
|
-
"""Click preview
|
|
219
|
+
"""Click preview then insert as image or background."""
|
|
233
220
|
_click_preview_image(page)
|
|
234
221
|
|
|
235
|
-
|
|
236
|
-
if insert_as == "background":
|
|
237
|
-
option_text = "Insert as background"
|
|
238
|
-
else:
|
|
239
|
-
option_text = "Insert as image"
|
|
222
|
+
option_text = "Insert as background" if insert_as == "background" else "Insert as image"
|
|
240
223
|
|
|
241
224
|
btn = page.get_by_text(option_text)
|
|
242
225
|
if btn.count() > 0 and btn.first.is_visible():
|
|
243
226
|
btn.first.click()
|
|
244
227
|
else:
|
|
245
|
-
# Fallback: try "Insert on new slide"
|
|
228
|
+
# Fallback: try "Insert on new slide"
|
|
246
229
|
fallback = page.get_by_text("Insert on new slide")
|
|
247
230
|
if fallback.count() > 0 and fallback.first.is_visible():
|
|
248
231
|
fallback.first.click()
|
|
@@ -252,15 +235,12 @@ def insert_image(page: Page, insert_as: str = "image") -> None:
|
|
|
252
235
|
page.wait_for_timeout(2000)
|
|
253
236
|
|
|
254
237
|
|
|
255
|
-
|
|
256
238
|
def check_url(page: Page, presentation_id: str) -> None:
|
|
257
|
-
"""Verify browser URL still contains the target presentation ID."""
|
|
258
239
|
if presentation_id not in page.url:
|
|
259
240
|
raise GenerationError("Browser navigated away from target presentation")
|
|
260
241
|
|
|
261
242
|
|
|
262
243
|
def _wait_for_new_slide(page: Page) -> None:
|
|
263
|
-
"""Wait for new slide to appear in filmstrip."""
|
|
264
244
|
page.wait_for_timeout(2000)
|
|
265
245
|
|
|
266
246
|
|
|
@@ -274,6 +254,15 @@ _INSERT_FN = {
|
|
|
274
254
|
}
|
|
275
255
|
|
|
276
256
|
|
|
257
|
+
def _require_login() -> "Path":
|
|
258
|
+
"""Return storage path or exit if not logged in."""
|
|
259
|
+
storage_path = get_storage_path()
|
|
260
|
+
if not storage_path.exists():
|
|
261
|
+
click.echo("Not logged in. Run: gslide auth login", err=True)
|
|
262
|
+
sys.exit(1)
|
|
263
|
+
return storage_path
|
|
264
|
+
|
|
265
|
+
|
|
277
266
|
def gen_single(
|
|
278
267
|
presentation_id: str,
|
|
279
268
|
tab: str,
|
|
@@ -284,11 +273,7 @@ def gen_single(
|
|
|
284
273
|
insert_as: str = "image",
|
|
285
274
|
) -> None:
|
|
286
275
|
"""Generate a single slide/infographic/image via browser automation."""
|
|
287
|
-
storage_path =
|
|
288
|
-
if not storage_path.exists():
|
|
289
|
-
click.echo("Not logged in. Run: gslide auth login", err=True)
|
|
290
|
-
sys.exit(1)
|
|
291
|
-
|
|
276
|
+
storage_path = _require_login()
|
|
292
277
|
timeout_ms = timeout * 1000
|
|
293
278
|
|
|
294
279
|
with BrowserSession(storage_state=storage_path) as context:
|
|
@@ -315,7 +300,6 @@ def gen_single(
|
|
|
315
300
|
|
|
316
301
|
except GenerationError as e:
|
|
317
302
|
click.echo(f"Error: {e}", err=True)
|
|
318
|
-
# Take screenshot for debugging
|
|
319
303
|
try:
|
|
320
304
|
page.screenshot(path="/tmp/gslide_error.png")
|
|
321
305
|
click.echo("Debug screenshot saved to /tmp/gslide_error.png")
|
|
@@ -344,11 +328,7 @@ def gen_batch(
|
|
|
344
328
|
timeout: int = 120,
|
|
345
329
|
) -> None:
|
|
346
330
|
"""Generate all slides from prompts data in a single browser session."""
|
|
347
|
-
storage_path =
|
|
348
|
-
if not storage_path.exists():
|
|
349
|
-
click.echo("Not logged in. Run: gslide auth login", err=True)
|
|
350
|
-
sys.exit(1)
|
|
351
|
-
|
|
331
|
+
storage_path = _require_login()
|
|
352
332
|
timeout_ms = timeout * 1000
|
|
353
333
|
total_slides = len(prompts_data.slides)
|
|
354
334
|
total_images = len(prompts_data.images)
|
|
@@ -397,7 +377,7 @@ def gen_batch(
|
|
|
397
377
|
page.wait_for_timeout(500)
|
|
398
378
|
_reopen_panel(page)
|
|
399
379
|
|
|
400
|
-
except
|
|
380
|
+
except Exception as e:
|
|
401
381
|
errors.append((i, slide.tab, str(e)))
|
|
402
382
|
click.echo(f" {label} FAILED: {e}")
|
|
403
383
|
if not continue_on_error:
|
|
@@ -412,7 +392,7 @@ def gen_batch(
|
|
|
412
392
|
check_url(page, prompts_data.presentation_id)
|
|
413
393
|
_navigate_to_slide(page, img.target_slide)
|
|
414
394
|
|
|
415
|
-
if "image"
|
|
395
|
+
if current_tab != "image":
|
|
416
396
|
select_tab(page, "Image")
|
|
417
397
|
current_tab = "image"
|
|
418
398
|
|
|
@@ -423,13 +403,12 @@ def gen_batch(
|
|
|
423
403
|
elapsed = time.monotonic() - t0
|
|
424
404
|
click.echo(f" {label} done ({elapsed:.1f}s)")
|
|
425
405
|
|
|
426
|
-
except
|
|
406
|
+
except Exception as e:
|
|
427
407
|
errors.append((total_slides + i, "image", str(e)))
|
|
428
408
|
click.echo(f" {label} FAILED: {e}")
|
|
429
409
|
if not continue_on_error:
|
|
430
410
|
break
|
|
431
411
|
|
|
432
|
-
|
|
433
412
|
# Summary
|
|
434
413
|
total_elapsed = time.monotonic() - start_time
|
|
435
414
|
total_ops = total_slides + total_images
|
package/src/gslide/prompts.py
CHANGED
|
@@ -33,9 +33,7 @@ class PromptsData:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def load_prompts(path: Path) -> PromptsData:
|
|
36
|
-
|
|
37
|
-
with open(path) as f:
|
|
38
|
-
raw = json.load(f)
|
|
36
|
+
raw = json.loads(path.read_text())
|
|
39
37
|
|
|
40
38
|
if not isinstance(raw, dict):
|
|
41
39
|
raise ValidationError("Prompts file must be a JSON object")
|