@champpaba/gslide 0.1.3 → 0.1.5
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 +9 -20
- package/src/gslide/browser.py +1 -2
- package/src/gslide/gen.py +32 -83
- 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
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
"""Authentication session management — file I/O and browser operations."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import sys
|
|
5
4
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
5
|
|
|
8
6
|
import click
|
|
9
7
|
|
|
10
|
-
|
|
11
8
|
SLIDES_URL = "https://docs.google.com/presentation/"
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
def get_storage_path() -> Path:
|
|
15
|
-
"""Return the path to the storage state file."""
|
|
16
12
|
return Path.home() / ".gslide" / "storage_state.json"
|
|
17
13
|
|
|
18
14
|
|
|
19
15
|
def is_logged_in() -> bool:
|
|
20
|
-
"""Check if a storage state file exists."""
|
|
21
16
|
return get_storage_path().exists()
|
|
22
17
|
|
|
23
18
|
|
|
24
|
-
def
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
def require_login() -> Path:
|
|
20
|
+
"""Return storage path or exit if not logged in."""
|
|
21
|
+
storage_path = get_storage_path()
|
|
22
|
+
if not storage_path.exists():
|
|
23
|
+
click.echo("Not logged in. Run: gslide auth login", err=True)
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
return storage_path
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
def delete_storage_state() -> None:
|
|
32
|
-
|
|
33
|
-
try:
|
|
34
|
-
get_storage_path().unlink()
|
|
35
|
-
except FileNotFoundError:
|
|
36
|
-
pass
|
|
29
|
+
get_storage_path().unlink(missing_ok=True)
|
|
37
30
|
|
|
38
31
|
|
|
39
32
|
def login() -> None:
|
|
@@ -64,11 +57,7 @@ def login() -> None:
|
|
|
64
57
|
|
|
65
58
|
def status() -> None:
|
|
66
59
|
"""Check if saved session is still valid."""
|
|
67
|
-
path =
|
|
68
|
-
|
|
69
|
-
if not path.exists():
|
|
70
|
-
click.echo("Not logged in. Run: gslide auth login")
|
|
71
|
-
sys.exit(1)
|
|
60
|
+
path = require_login()
|
|
72
61
|
|
|
73
62
|
from gslide.browser import BrowserSession
|
|
74
63
|
|
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
|
@@ -16,11 +16,10 @@ import time
|
|
|
16
16
|
import click
|
|
17
17
|
from playwright.sync_api import Page, TimeoutError as PwTimeout
|
|
18
18
|
|
|
19
|
-
from gslide.auth import
|
|
19
|
+
from gslide.auth import require_login
|
|
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,16 +70,10 @@ 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
|
-
|
|
82
|
-
hmv.click()
|
|
83
|
-
page.wait_for_selector('[role="tab"]', timeout=PANEL_LOAD_TIMEOUT_MS)
|
|
76
|
+
open_panel(page)
|
|
84
77
|
return
|
|
85
78
|
except (PwTimeout, Exception):
|
|
86
79
|
if attempt < retries:
|
|
@@ -97,12 +90,9 @@ def select_tab(page: Page, tab_name: str) -> None:
|
|
|
97
90
|
|
|
98
91
|
|
|
99
92
|
def _find_visible_textarea(page: Page) -> object:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
ta = textareas.nth(i)
|
|
104
|
-
if ta.is_visible():
|
|
105
|
-
return ta
|
|
93
|
+
ta = page.locator("textarea").first
|
|
94
|
+
if ta.is_visible():
|
|
95
|
+
return ta
|
|
106
96
|
raise GenerationError("No visible text input found in panel")
|
|
107
97
|
|
|
108
98
|
|
|
@@ -179,12 +169,7 @@ def fill_and_create(
|
|
|
179
169
|
|
|
180
170
|
|
|
181
171
|
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
|
-
"""
|
|
172
|
+
"""Click the generated preview image to open the insert overlay."""
|
|
188
173
|
preview = page.locator('img[src*="googleusercontent.com"]')
|
|
189
174
|
for i in range(preview.count()):
|
|
190
175
|
try:
|
|
@@ -203,73 +188,48 @@ def _click_preview_image(page: Page) -> None:
|
|
|
203
188
|
raise GenerationError("Generated preview image not found")
|
|
204
189
|
|
|
205
190
|
|
|
206
|
-
def
|
|
207
|
-
"""Click
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
btn = page.get_by_text("Insert on new slide")
|
|
191
|
+
def _click_insert_button(page: Page, text: str) -> None:
|
|
192
|
+
"""Click an insert button by text, raising GenerationError if not found."""
|
|
193
|
+
btn = page.get_by_text(text)
|
|
211
194
|
if btn.count() > 0 and btn.first.is_visible():
|
|
212
195
|
btn.first.click()
|
|
213
196
|
else:
|
|
214
|
-
raise GenerationError("'
|
|
215
|
-
_wait_for_new_slide(page)
|
|
197
|
+
raise GenerationError(f"'{text}' button not found after clicking preview")
|
|
216
198
|
|
|
217
199
|
|
|
218
|
-
def
|
|
219
|
-
"""Click preview
|
|
200
|
+
def _insert_on_new_slide(page: Page) -> None:
|
|
201
|
+
"""Click preview then 'Insert on new slide' (used by both infographic and slide tabs)."""
|
|
220
202
|
_click_preview_image(page)
|
|
221
|
-
|
|
222
|
-
|
|
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")
|
|
228
|
-
_wait_for_new_slide(page)
|
|
203
|
+
_click_insert_button(page, "Insert on new slide")
|
|
204
|
+
page.wait_for_timeout(2000)
|
|
229
205
|
|
|
230
206
|
|
|
231
207
|
def insert_image(page: Page, insert_as: str = "image") -> None:
|
|
232
|
-
"""Click preview
|
|
208
|
+
"""Click preview then insert as image or background."""
|
|
233
209
|
_click_preview_image(page)
|
|
234
210
|
|
|
235
|
-
|
|
236
|
-
if insert_as == "background":
|
|
237
|
-
option_text = "Insert as background"
|
|
238
|
-
else:
|
|
239
|
-
option_text = "Insert as image"
|
|
211
|
+
option_text = "Insert as background" if insert_as == "background" else "Insert as image"
|
|
240
212
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
fallback = page.get_by_text("Insert on new slide")
|
|
247
|
-
if fallback.count() > 0 and fallback.first.is_visible():
|
|
248
|
-
fallback.first.click()
|
|
249
|
-
else:
|
|
250
|
-
raise GenerationError(f"'{option_text}' button not found")
|
|
213
|
+
try:
|
|
214
|
+
_click_insert_button(page, option_text)
|
|
215
|
+
except GenerationError:
|
|
216
|
+
# Fallback: try "Insert on new slide"
|
|
217
|
+
_click_insert_button(page, "Insert on new slide")
|
|
251
218
|
|
|
252
219
|
page.wait_for_timeout(2000)
|
|
253
220
|
|
|
254
221
|
|
|
255
|
-
|
|
256
222
|
def check_url(page: Page, presentation_id: str) -> None:
|
|
257
|
-
"""Verify browser URL still contains the target presentation ID."""
|
|
258
223
|
if presentation_id not in page.url:
|
|
259
224
|
raise GenerationError("Browser navigated away from target presentation")
|
|
260
225
|
|
|
261
226
|
|
|
262
|
-
def _wait_for_new_slide(page: Page) -> None:
|
|
263
|
-
"""Wait for new slide to appear in filmstrip."""
|
|
264
|
-
page.wait_for_timeout(2000)
|
|
265
|
-
|
|
266
|
-
|
|
267
227
|
# --- Orchestration ---
|
|
268
228
|
|
|
269
229
|
|
|
270
230
|
_INSERT_FN = {
|
|
271
|
-
"infographic": lambda page, **_:
|
|
272
|
-
"slide": lambda page, **_:
|
|
231
|
+
"infographic": lambda page, **_: _insert_on_new_slide(page),
|
|
232
|
+
"slide": lambda page, **_: _insert_on_new_slide(page),
|
|
273
233
|
"image": lambda page, **opts: insert_image(page, opts.get("insert_as", "image")),
|
|
274
234
|
}
|
|
275
235
|
|
|
@@ -284,11 +244,7 @@ def gen_single(
|
|
|
284
244
|
insert_as: str = "image",
|
|
285
245
|
) -> None:
|
|
286
246
|
"""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
|
-
|
|
247
|
+
storage_path = require_login()
|
|
292
248
|
timeout_ms = timeout * 1000
|
|
293
249
|
|
|
294
250
|
with BrowserSession(storage_state=storage_path) as context:
|
|
@@ -296,7 +252,6 @@ def gen_single(
|
|
|
296
252
|
|
|
297
253
|
try:
|
|
298
254
|
navigate_to_presentation(page, presentation_id)
|
|
299
|
-
check_url(page, presentation_id)
|
|
300
255
|
|
|
301
256
|
if tab == "image" and slide_index is not None:
|
|
302
257
|
_navigate_to_slide(page, slide_index)
|
|
@@ -315,7 +270,6 @@ def gen_single(
|
|
|
315
270
|
|
|
316
271
|
except GenerationError as e:
|
|
317
272
|
click.echo(f"Error: {e}", err=True)
|
|
318
|
-
# Take screenshot for debugging
|
|
319
273
|
try:
|
|
320
274
|
page.screenshot(path="/tmp/gslide_error.png")
|
|
321
275
|
click.echo("Debug screenshot saved to /tmp/gslide_error.png")
|
|
@@ -344,11 +298,7 @@ def gen_batch(
|
|
|
344
298
|
timeout: int = 120,
|
|
345
299
|
) -> None:
|
|
346
300
|
"""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
|
-
|
|
301
|
+
storage_path = require_login()
|
|
352
302
|
timeout_ms = timeout * 1000
|
|
353
303
|
total_slides = len(prompts_data.slides)
|
|
354
304
|
total_images = len(prompts_data.images)
|
|
@@ -397,7 +347,7 @@ def gen_batch(
|
|
|
397
347
|
page.wait_for_timeout(500)
|
|
398
348
|
_reopen_panel(page)
|
|
399
349
|
|
|
400
|
-
except
|
|
350
|
+
except Exception as e:
|
|
401
351
|
errors.append((i, slide.tab, str(e)))
|
|
402
352
|
click.echo(f" {label} FAILED: {e}")
|
|
403
353
|
if not continue_on_error:
|
|
@@ -412,7 +362,7 @@ def gen_batch(
|
|
|
412
362
|
check_url(page, prompts_data.presentation_id)
|
|
413
363
|
_navigate_to_slide(page, img.target_slide)
|
|
414
364
|
|
|
415
|
-
if "image"
|
|
365
|
+
if current_tab != "image":
|
|
416
366
|
select_tab(page, "Image")
|
|
417
367
|
current_tab = "image"
|
|
418
368
|
|
|
@@ -423,13 +373,12 @@ def gen_batch(
|
|
|
423
373
|
elapsed = time.monotonic() - t0
|
|
424
374
|
click.echo(f" {label} done ({elapsed:.1f}s)")
|
|
425
375
|
|
|
426
|
-
except
|
|
376
|
+
except Exception as e:
|
|
427
377
|
errors.append((total_slides + i, "image", str(e)))
|
|
428
378
|
click.echo(f" {label} FAILED: {e}")
|
|
429
379
|
if not continue_on_error:
|
|
430
380
|
break
|
|
431
381
|
|
|
432
|
-
|
|
433
382
|
# Summary
|
|
434
383
|
total_elapsed = time.monotonic() - start_time
|
|
435
384
|
total_ops = total_slides + total_images
|
|
@@ -442,5 +391,5 @@ def gen_batch(
|
|
|
442
391
|
for idx, tab, err in errors:
|
|
443
392
|
click.echo(f" #{idx} ({tab}): {err}")
|
|
444
393
|
click.echo(
|
|
445
|
-
f"Presentation:
|
|
394
|
+
f"Presentation: {PRESENTATION_URL.format(id=prompts_data.presentation_id)}"
|
|
446
395
|
)
|
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")
|