
Notebooklm Superskill
- 51 installs
- 3 repo stars
- Updated December 31, 2025
- ainergiz/notebooklm-superskill
notebooklm-superskill is a Claude Code skill that generates slides, podcasts, infographics, and videos from a Google NotebookLM notebook.
About
notebooklm-superskill is a Claude Code skill that generates slide decks, audio podcasts, infographics, and video overviews from a Google NotebookLM notebook. It runs Python scripts through a run.py wrapper that automate NotebookLM in a browser, with options for audience, format, orientation, theme, and language. Developers use it to turn research notebooks into shareable content for different audiences.
- Generates slide decks, audio podcasts, infographics, and video overviews from NotebookLM
- Audience, format, orientation, theme, and 80+ language customization
- Drives NotebookLM via browser automation after one-time Google auth
Notebooklm Superskill by the numbers
- 51 all-time installs (skills.sh)
- Ranked #893 of 1,337 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
notebooklm-superskill capabilities & compatibility
- Capabilities
- slide generation · audio generation · infographic generation · video generation
- Works with
- google drive
- Use cases
- presentations · image generation · video generation · copywriting
- Pricing
- Bring your own API key
What notebooklm-superskill says it does
Generate professional content from NotebookLM notebooks: slides, podcasts, infographics, and videos.
Audio generation takes 5-10 minutes.
npx skills add https://github.com/ainergiz/notebooklm-superskill --skill notebooklm-superskillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 51 |
|---|---|
| repo stars | ★ 3 |
| Last updated | December 31, 2025 |
| Repository | ainergiz/notebooklm-superskill ↗ |
What it does
Generate slides, podcasts, infographics, and videos from a NotebookLM notebook URL.
Who is it for?
Developers turning a NotebookLM research notebook into decks, podcasts, or videos for specific audiences.
Skip if: Content creation without a NotebookLM notebook URL or Google account.
When should I use this skill?
When asked to generate slides, create a podcast, make an infographic, or a video overview from NotebookLM.
What you get
Ready slide decks, podcast audio, infographics, and video overviews in the chosen format and language.
- slide decks
- podcast audio
- infographics
By the numbers
- 4 output formats
- 80+ languages
- 5 slide audiences (technical, investor, customer, executive, beginner)
Files
NotebookLM SuperSkill
Generate professional content from NotebookLM notebooks: slides, podcasts, infographics, and videos.
When to Use This Skill
Use this skill when the user wants to:
- Generate slide decks for different audiences (technical, investor, customer, executive, beginner)
- Create AI podcast-style audio overviews
- Make visual infographics from research
- Generate video overviews/explainers
- Automate NotebookLM content creation
Critical: Always Use run.py Wrapper
All scripts MUST be run through run.py to ensure proper virtual environment setup:
python scripts/run.py <script_name> [args...]Authentication (One-Time Setup)
Before first use, authenticate with Google:
# Interactive login (browser opens)
python scripts/run.py auth_manager.py setup
# Check status
python scripts/run.py auth_manager.py status
# Validate credentials work
python scripts/run.py auth_manager.py validateQuick Reference
1. Slide Decks
Generate presentation slides with audience-specific customization.
# Single slide deck
python scripts/run.py generate_slides.py \
--notebook-url "https://notebooklm.google.com/notebook/..." \
--audience technical \
--format detailed \
--length default
# Multiple audiences at once
python scripts/run.py generate_slides.py \
--notebook-url URL \
--audiences technical,investor,customerOptions:
| Option | Values | Description |
|---|---|---|
--audience | technical, investor, customer, executive, beginner | Target audience |
--audiences | comma-separated | Generate multiple decks |
--format | detailed, presenter | Slide format |
--length | short, default, long | Slide count |
--source | path | Upload source file first |
--prompt | text | Custom prompt (overrides audience) |
2. Audio Overviews (Podcasts)
Generate AI podcast-style deep dive discussions.
python scripts/run.py generate_audio.py \
--notebook-url URL \
--format deep-dive \
--language en-USOptions:
| Option | Values | Description |
|---|---|---|
--format | deep-dive, brief, critique, debate | Podcast style |
--language | en-US, es-ES, fr-FR, de-DE, ja-JP, etc. | 80+ languages |
--prompt | text | Custom instructions |
Note: Audio generation takes 5-10 minutes.
3. Infographics
Generate visual infographics for different platforms.
python scripts/run.py generate_infographic.py \
--notebook-url URL \
--orientation landscape \
--detail standardOptions:
| Option | Values | Description |
|---|---|---|
--orientation | square, portrait, landscape | Aspect ratio |
--detail | concise, standard, detailed | Information density |
--prompt | text | Custom instructions |
Orientations:
square- 1:1 for social media postsportrait- 9:16 for Instagram Stories, TikToklandscape- 16:9 for LinkedIn, presentations
4. Video Overviews
Generate video explainers with visual themes.
python scripts/run.py generate_video.py \
--notebook-url URL \
--format explainer \
--theme futuristicOptions:
| Option | Values | Description |
|---|---|---|
--format | brief, explainer | Video length |
--theme | retro-90s, futuristic, corporate, minimal | Visual style |
--custom-theme | text | Custom theme description |
--prompt | text | Custom instructions |
Note: Video generation takes 10-15 minutes.
Decision Flow
1. User wants slides? → Use generate_slides.py
- Multiple audiences? Use
--audiencesflag - Single audience? Use
--audienceflag
2. User wants podcast/audio? → Use generate_audio.py
- Non-English? Specify
--language - Quick summary? Use
--format brief
3. User wants visual summary? → Use generate_infographic.py
- Social media? Consider
--orientation portrait - Presentation? Use
--orientation landscape
4. User wants video? → Use generate_video.py
- Quick overview? Use
--format brief - Training material? Use
--format explainer
Common Options
All scripts support:
--output DIR- Output directory (default: current)--headless- Run without visible browser--help- Show detailed help
Example Workflows
Investor Pitch Materials
# Generate investor slides
python scripts/run.py generate_slides.py --notebook-url URL --audience investor
# Create brief overview podcast
python scripts/run.py generate_audio.py --notebook-url URL --format brief
# Make landscape infographic for deck
python scripts/run.py generate_infographic.py --notebook-url URL --orientation landscapeTraining Content
# Beginner-friendly slides
python scripts/run.py generate_slides.py --notebook-url URL --audience beginner --length long
# Deep dive podcast
python scripts/run.py generate_audio.py --notebook-url URL --format deep-dive
# Explainer video
python scripts/run.py generate_video.py --notebook-url URL --format explainerSocial Media Content
# Portrait infographic for stories
python scripts/run.py generate_infographic.py --notebook-url URL --orientation portrait --detail concise
# Brief video with trendy theme
python scripts/run.py generate_video.py --notebook-url URL --format brief --theme retro-90sTroubleshooting
Authentication issues:
python scripts/run.py auth_manager.py reauthScript not found:
- Ensure you're in the skill directory
- Use full path:
python /path/to/scripts/run.py ...
Timeout errors:
- Audio/video generation takes time (5-15 min)
- Use
--headlessfor faster execution - Check notebook has sufficient source content
Additional Resources
For detailed guides on each feature, see:
- references/slides_guide.md
- references/audio_guide.md
- references/infographic_guide.md
- references/video_guide.md
# Runtime data
data/
downloads/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv/
venv/
ENV/
env/
.env
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
NotebookLM SuperSkill
Generate slides, podcasts, infographics, and videos from NotebookLM notebooks using browser automation.
Features
- Slide Decks - Generate presentations for different audiences (technical, investor, customer, executive, beginner)
- Audio Overviews - Create AI podcast-style discussions in 80+ languages
- Infographics - Visual summaries in multiple orientations
- Video Overviews - Explainer videos with customizable themes
Installation
As a Claude Code Skill
1. Clone this repo to your skills directory:
git clone https://github.com/ainergiz/notebooklm-superskill.git ~/.claude/skills/notebooklm-superskill2. Restart Claude Code to load the skill.
Standalone Usage
1. Clone the repository:
git clone https://github.com/ainergiz/notebooklm-superskill.git
cd notebooklm-superskill2. Run any script (virtual environment is set up automatically):
python scripts/run.py auth_manager.py setupQuick Start
1. Authenticate (One-Time)
python scripts/run.py auth_manager.py setupA browser window opens. Log in to your Google account. The session is saved for future use.
2. Generate Content
# Slides
python scripts/run.py generate_slides.py --notebook-url URL --audience technical
# Audio podcast
python scripts/run.py generate_audio.py --notebook-url URL --format deep-dive
# Infographic
python scripts/run.py generate_infographic.py --notebook-url URL --orientation landscape
# Video
python scripts/run.py generate_video.py --notebook-url URL --format explainerOptions Reference
Slide Generation
| Option | Values | Default |
|---|---|---|
--audience | technical, investor, customer, executive, beginner | technical |
--audiences | comma-separated list | - |
--format | detailed, presenter | detailed |
--length | short, default, long | default |
--source | file path | - |
--prompt | custom text | - |
Audio Generation
| Option | Values | Default |
|---|---|---|
--format | deep-dive, brief, critique, debate | deep-dive |
--language | en-US, es-ES, fr-FR, etc. | en-US |
--prompt | custom text | - |
Infographic Generation
| Option | Values | Default |
|---|---|---|
--orientation | square, portrait, landscape | landscape |
--detail | concise, standard, detailed | standard |
--prompt | custom text | - |
Video Generation
| Option | Values | Default |
|---|---|---|
--format | brief, explainer | brief |
--theme | retro-90s, futuristic, corporate, minimal | corporate |
--custom-theme | custom description | - |
--prompt | custom text | - |
Common Options
All scripts support:
--output DIR- Output directory--headless- Run without visible browser--help- Show help
Architecture
notebooklm-superskill/
├── SKILL.md # Claude Code skill file
├── scripts/
│ ├── run.py # Universal wrapper (handles venv)
│ ├── auth_manager.py # Authentication
│ ├── generate_slides.py
│ ├── generate_audio.py
│ ├── generate_infographic.py
│ └── generate_video.py
└── data/ # Runtime data (gitignored)
└── browser_state/ # Cookies, profileHow It Works
1. Browser Automation - Uses Patchright (Playwright fork) with real Chrome for reliability 2. Anti-Detection - Persistent browser profile, cookie injection, human-like interactions 3. Artifact Monitoring - Detects generation completion via shimmer animation detection 4. Download Handling - Captures downloads via Playwright's download handler
Requirements
- Python 3.8+
- Google account with NotebookLM access
- Chrome browser (installed automatically by Patchright)
Troubleshooting
Authentication Expired
python scripts/run.py auth_manager.py reauthTimeout During Generation
Audio and video generation can take 5-15 minutes. The scripts have built-in timeouts:
- Slides/Infographics: 3 minutes
- Audio: 10 minutes
- Video: 15 minutes
Browser Issues
# Reinstall Chrome for Patchright
python -m patchright install chromeLicense
MIT
Slide Deck Generation Guide
Detailed guide for generating slide decks from NotebookLM.
Audience Types
Technical
For software engineers and developers. Includes:
- Architecture diagrams
- Implementation details
- Technical specifications
- Code examples
Investor
For investors and stakeholders. Includes:
- Market opportunity
- Business model
- Competitive advantages
- Financial projections
- Growth potential
Customer
For end users and customers. Includes:
- Benefits and value proposition
- Problem-solution fit
- Ease of use
- Success stories
Executive
For C-level decision makers. Includes:
- Strategic value
- ROI metrics
- High-level roadmap
- Key metrics
Beginner
For newcomers to the topic. Includes:
- Fundamentals
- Step-by-step explanations
- Simple examples
- Educational approach
Format Options
Detailed Deck
Full presentation with comprehensive content. Best for:
- Standalone presentations
- Training materials
- Documentation
Presenter Slides
Minimal slides for live presentations. Best for:
- Keynotes
- Meetings
- Workshops
Length Options
| Option | Approximate Slides |
|---|---|
| Short | 5-10 slides |
| Default | 10-20 slides |
| Long | 20-30 slides |
Examples
Generate for All Business Audiences
python scripts/run.py generate_slides.py \
--notebook-url URL \
--audiences investor,customer,executiveUpload Source and Generate
python scripts/run.py generate_slides.py \
--notebook-url URL \
--source requirements.md \
--audience technical \
--format detailed \
--length longCustom Prompt
python scripts/run.py generate_slides.py \
--notebook-url URL \
--prompt "Create a security-focused presentation highlighting compliance and data protection measures"patchright==1.55.2
python-dotenv==1.0.0
"""NotebookLM SuperSkill - Generate slides, audio, infographics, and video from NotebookLM."""
__version__ = "1.0.0"
"""
Artifact Monitor for NotebookLM SuperSkill
Monitors artifact generation status using shimmer detection
"""
import time
from typing import Optional, List, Dict, Any
from patchright.sync_api import Page, ElementHandle
from config import ARTIFACT_SELECTORS, ARTIFACT_TIMEOUT_SECONDS
class ArtifactMonitor:
"""Monitor NotebookLM artifact generation status"""
def __init__(self, page: Page, artifact_type: str = "slides"):
"""
Initialize artifact monitor.
Args:
page: Playwright page
artifact_type: Type of artifact (slides, audio, infographic, video)
"""
self.page = page
self.artifact_type = artifact_type
self.selector = ARTIFACT_SELECTORS.get(artifact_type, ARTIFACT_SELECTORS["slides"])
def count_artifacts(self) -> int:
"""Count current artifacts of this type in Studio panel"""
try:
artifacts = self.page.query_selector_all(self.selector)
return len(artifacts)
except Exception:
return 0
def get_artifacts(self) -> List[ElementHandle]:
"""Get all artifact elements of this type"""
try:
return self.page.query_selector_all(self.selector)
except Exception:
return []
def is_shimmer_loading(self, artifact: ElementHandle) -> bool:
"""
Check if artifact is still loading (shimmer animation active).
NotebookLM uses a 'shimmer' CSS class on the parent container
to indicate loading state.
Args:
artifact: Artifact element handle
Returns:
True if artifact is still loading
"""
try:
parent = artifact.evaluate_handle("el => el.closest('.artifact-item-button')")
if parent:
classes = parent.evaluate("el => el.className") or ""
return "shimmer" in classes
except Exception:
pass
return False
def get_loading_count(self) -> int:
"""Count how many artifacts are currently loading"""
artifacts = self.get_artifacts()
return sum(1 for a in artifacts if self.is_shimmer_loading(a))
def get_ready_count(self) -> int:
"""Count how many artifacts are ready (not loading)"""
artifacts = self.get_artifacts()
return sum(1 for a in artifacts if not self.is_shimmer_loading(a))
def wait_for_artifact_ready(
self,
initial_count: int,
timeout_sec: int = ARTIFACT_TIMEOUT_SECONDS
) -> bool:
"""
Wait for a NEW artifact to appear AND finish loading.
Uses:
- Artifact counting for progress tracking
- Shimmer detection for completion
- Stability polling (artifact present without shimmer)
Args:
initial_count: Number of artifacts before generation started
timeout_sec: Maximum wait time in seconds
Returns:
True if new artifact is ready, False on timeout
"""
print(f" Waiting for {self.artifact_type} to generate... (initial count: {initial_count})")
artifact_appeared = False
poll_interval = 2
for i in range(timeout_sec // poll_interval):
time.sleep(poll_interval)
current_count = self.count_artifacts()
if current_count > initial_count:
if not artifact_appeared:
print(f" New artifact appeared! (count: {current_count})")
artifact_appeared = True
# Check if the latest artifact is still loading
artifacts = self.get_artifacts()
if artifacts:
latest = artifacts[-1]
if not self.is_shimmer_loading(latest):
elapsed = i * poll_interval
print(f" Artifact ready! (took ~{elapsed}s)")
return True
elif i % 10 == 0:
print(f" Still loading... ({i * poll_interval}s)")
elif i % 15 == 0 and i > 0 and not artifact_appeared:
print(f" Still generating... ({i * poll_interval}s)")
print(f" Timeout waiting for {self.artifact_type}")
return False
def wait_for_multiple_ready(
self,
expected_count: int,
timeout_sec: int = ARTIFACT_TIMEOUT_SECONDS
) -> int:
"""
Wait for multiple artifacts to be ready.
Args:
expected_count: Expected total artifact count
timeout_sec: Maximum wait time
Returns:
Number of ready artifacts
"""
print(f" Waiting for {expected_count} {self.artifact_type} to be ready...")
poll_interval = 2
for i in range(timeout_sec // poll_interval):
time.sleep(poll_interval)
current_count = self.count_artifacts()
loading_count = self.get_loading_count()
ready_count = current_count - loading_count
if i % 10 == 0:
print(f" [{i * poll_interval}s] Total: {current_count}, Ready: {ready_count}, Loading: {loading_count}")
# All done when we have expected count and none are loading
if current_count >= expected_count and loading_count == 0:
print(f" All {current_count} artifacts ready!")
return ready_count
# Return what we have
return self.get_ready_count()
def get_artifact_info(self, artifact: ElementHandle) -> Dict[str, Any]:
"""
Get information about an artifact.
Args:
artifact: Artifact element
Returns:
Dict with artifact info (title, loading status, etc.)
"""
try:
title_el = artifact.query_selector(".artifact-title")
title = title_el.text_content() if title_el else "Untitled"
return {
"title": title.strip() if title else "Untitled",
"loading": self.is_shimmer_loading(artifact),
"type": self.artifact_type,
}
except Exception:
return {
"title": "Unknown",
"loading": False,
"type": self.artifact_type,
}
#!/usr/bin/env python3
"""
Authentication Manager for NotebookLM SuperSkill
Handles Google login and browser state persistence
Implements hybrid auth approach:
- Persistent browser profile for fingerprint consistency
- Manual cookie injection from state.json (Playwright bug workaround)
See: https://github.com/microsoft/playwright/issues/36139
"""
import json
import time
import argparse
import shutil
import re
import sys
from pathlib import Path
from typing import Dict, Any
from patchright.sync_api import sync_playwright, BrowserContext
sys.path.insert(0, str(Path(__file__).parent))
from config import BROWSER_STATE_DIR, STATE_FILE, AUTH_INFO_FILE, DATA_DIR
from browser_factory import BrowserFactory
class AuthManager:
"""
Manages authentication and browser state for NotebookLM
Features:
- Interactive Google login
- Browser state persistence
- Session restoration
- Authentication validation
"""
def __init__(self):
"""Initialize the authentication manager"""
DATA_DIR.mkdir(parents=True, exist_ok=True)
BROWSER_STATE_DIR.mkdir(parents=True, exist_ok=True)
self.state_file = STATE_FILE
self.auth_info_file = AUTH_INFO_FILE
self.browser_state_dir = BROWSER_STATE_DIR
def is_authenticated(self) -> bool:
"""Check if valid authentication exists"""
if not self.state_file.exists():
return False
# Check if state file is not too old (7 days)
age_days = (time.time() - self.state_file.stat().st_mtime) / 86400
if age_days > 7:
print(f" Browser state is {age_days:.1f} days old, may need re-authentication")
return True
def get_auth_info(self) -> Dict[str, Any]:
"""Get authentication information"""
info = {
'authenticated': self.is_authenticated(),
'state_file': str(self.state_file),
'state_exists': self.state_file.exists()
}
if self.auth_info_file.exists():
try:
with open(self.auth_info_file, 'r') as f:
saved_info = json.load(f)
info.update(saved_info)
except Exception:
pass
if info['state_exists']:
age_hours = (time.time() - self.state_file.stat().st_mtime) / 3600
info['state_age_hours'] = age_hours
return info
def setup_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool:
"""
Perform interactive authentication setup.
IMPORTANT: Browser must be visible (headless=False) for login.
Args:
headless: Run browser in headless mode (False for login)
timeout_minutes: Maximum time to wait for login
Returns:
True if authentication successful
"""
print("Starting authentication setup...")
print(f" Timeout: {timeout_minutes} minutes")
playwright = None
context = None
try:
playwright = sync_playwright().start()
context = BrowserFactory.launch_persistent_context(
playwright,
headless=headless
)
page = context.new_page()
page.goto("https://notebooklm.google.com", wait_until="domcontentloaded")
# Check if already authenticated
if "notebooklm.google.com" in page.url and "accounts.google.com" not in page.url:
print(" Already authenticated!")
self._save_browser_state(context)
return True
# Wait for manual login
print("\n Please log in to your Google account...")
print(f" Waiting up to {timeout_minutes} minutes for login...")
try:
timeout_ms = int(timeout_minutes * 60 * 1000)
page.wait_for_url(re.compile(r"^https://notebooklm\.google\.com/"), timeout=timeout_ms)
print(" Login successful!")
self._save_browser_state(context)
self._save_auth_info()
return True
except Exception as e:
print(f" Authentication timeout: {e}")
return False
except Exception as e:
print(f" Error: {e}")
return False
finally:
if context:
try:
context.close()
except Exception:
pass
if playwright:
try:
playwright.stop()
except Exception:
pass
def _save_browser_state(self, context: BrowserContext):
"""Save browser state to disk"""
try:
context.storage_state(path=str(self.state_file))
print(f" Saved browser state to: {self.state_file}")
except Exception as e:
print(f" Failed to save browser state: {e}")
raise
def _save_auth_info(self):
"""Save authentication metadata"""
try:
info = {
'authenticated_at': time.time(),
'authenticated_at_iso': time.strftime('%Y-%m-%d %H:%M:%S')
}
with open(self.auth_info_file, 'w') as f:
json.dump(info, f, indent=2)
except Exception:
pass
def clear_auth(self) -> bool:
"""Clear all authentication data"""
print("Clearing authentication data...")
try:
if self.state_file.exists():
self.state_file.unlink()
print(" Removed browser state")
if self.auth_info_file.exists():
self.auth_info_file.unlink()
print(" Removed auth info")
if self.browser_state_dir.exists():
shutil.rmtree(self.browser_state_dir)
self.browser_state_dir.mkdir(parents=True, exist_ok=True)
print(" Cleared browser data")
return True
except Exception as e:
print(f" Error clearing auth: {e}")
return False
def re_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool:
"""Perform re-authentication (clear and setup)"""
print("Starting re-authentication...")
self.clear_auth()
return self.setup_auth(headless, timeout_minutes)
def validate_auth(self) -> bool:
"""Validate that stored authentication works"""
if not self.is_authenticated():
return False
print("Validating authentication...")
playwright = None
context = None
try:
playwright = sync_playwright().start()
context = BrowserFactory.launch_persistent_context(
playwright,
headless=True
)
page = context.new_page()
page.goto("https://notebooklm.google.com", wait_until="domcontentloaded", timeout=30000)
if "notebooklm.google.com" in page.url and "accounts.google.com" not in page.url:
print(" Authentication is valid")
return True
else:
print(" Authentication is invalid (redirected to login)")
return False
except Exception as e:
print(f" Validation failed: {e}")
return False
finally:
if context:
try:
context.close()
except Exception:
pass
if playwright:
try:
playwright.stop()
except Exception:
pass
def main():
"""Command-line interface for authentication management"""
parser = argparse.ArgumentParser(description='Manage NotebookLM authentication')
subparsers = parser.add_subparsers(dest='command', help='Commands')
setup_parser = subparsers.add_parser('setup', help='Setup authentication')
setup_parser.add_argument('--headless', action='store_true', help='Run in headless mode')
setup_parser.add_argument('--timeout', type=float, default=10, help='Login timeout in minutes')
subparsers.add_parser('status', help='Check authentication status')
subparsers.add_parser('validate', help='Validate authentication')
subparsers.add_parser('clear', help='Clear authentication')
reauth_parser = subparsers.add_parser('reauth', help='Re-authenticate')
reauth_parser.add_argument('--timeout', type=float, default=10, help='Login timeout in minutes')
args = parser.parse_args()
auth = AuthManager()
if args.command == 'setup':
if auth.setup_auth(headless=args.headless, timeout_minutes=args.timeout):
print("\nAuthentication setup complete!")
else:
print("\nAuthentication setup failed")
exit(1)
elif args.command == 'status':
info = auth.get_auth_info()
print("\nAuthentication Status:")
print(f" Authenticated: {'Yes' if info['authenticated'] else 'No'}")
if info.get('state_age_hours'):
print(f" State age: {info['state_age_hours']:.1f} hours")
if info.get('authenticated_at_iso'):
print(f" Last auth: {info['authenticated_at_iso']}")
elif args.command == 'validate':
if auth.validate_auth():
print("Authentication is valid and working")
else:
print("Authentication is invalid or expired")
print("Run: python run.py auth_manager.py setup")
elif args.command == 'clear':
if auth.clear_auth():
print("Authentication cleared")
elif args.command == 'reauth':
if auth.re_auth(timeout_minutes=args.timeout):
print("\nRe-authentication complete!")
else:
print("\nRe-authentication failed")
exit(1)
else:
parser.print_help()
if __name__ == "__main__":
main()
"""
Browser Factory for NotebookLM SuperSkill
Handles browser launching with anti-detection features
"""
import json
from typing import Optional
from patchright.sync_api import Playwright, BrowserContext
from config import BROWSER_PROFILE_DIR, STATE_FILE, BROWSER_ARGS, USER_AGENT
class BrowserFactory:
"""Factory for creating configured browser contexts with anti-detection"""
@staticmethod
def launch_persistent_context(
playwright: Playwright,
headless: bool = True,
user_data_dir: str = str(BROWSER_PROFILE_DIR)
) -> BrowserContext:
"""
Launch a persistent browser context with anti-detection features.
Uses:
- Real Chrome (not Chromium) for reliability
- Persistent profile for fingerprint consistency
- Cookie injection workaround for Playwright bug #36139
Args:
playwright: Playwright instance
headless: Run in headless mode
user_data_dir: Path to browser profile directory
Returns:
Configured BrowserContext
"""
context = playwright.chromium.launch_persistent_context(
user_data_dir=user_data_dir,
channel="chrome", # Use real Chrome
headless=headless,
no_viewport=True,
ignore_default_args=["--enable-automation"],
user_agent=USER_AGENT,
args=BROWSER_ARGS
)
# Cookie Workaround for Playwright bug #36139
# Session cookies (expires=-1) don't persist in user_data_dir automatically
BrowserFactory._inject_cookies(context)
return context
@staticmethod
def _inject_cookies(context: BrowserContext):
"""Inject cookies from state.json if available"""
if STATE_FILE.exists():
try:
with open(STATE_FILE, 'r') as f:
state = json.load(f)
if 'cookies' in state and len(state['cookies']) > 0:
context.add_cookies(state['cookies'])
except Exception:
pass # Non-critical - cookies might not be needed
"""
Configuration for NotebookLM SuperSkill
Centralizes paths, selectors, timeouts, and browser settings
"""
from pathlib import Path
# Paths
SKILL_DIR = Path(__file__).parent.parent
DATA_DIR = SKILL_DIR / "data"
BROWSER_STATE_DIR = DATA_DIR / "browser_state"
BROWSER_PROFILE_DIR = BROWSER_STATE_DIR / "browser_profile"
STATE_FILE = BROWSER_STATE_DIR / "state.json"
AUTH_INFO_FILE = DATA_DIR / "auth_info.json"
# Browser Configuration
BROWSER_ARGS = [
'--disable-blink-features=AutomationControlled', # Patches navigator.webdriver
'--disable-dev-shm-usage',
'--no-sandbox',
'--no-first-run',
'--no-default-browser-check'
]
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
# Timeouts (seconds unless noted)
LOGIN_TIMEOUT_MINUTES = 10
ARTIFACT_TIMEOUT_SECONDS = 180 # Slides, infographics
AUDIO_TIMEOUT_SECONDS = 600 # Podcasts take longer (10 min)
VIDEO_TIMEOUT_SECONDS = 900 # Videos take longest (15 min)
DOWNLOAD_TIMEOUT_MS = 60000
PAGE_LOAD_TIMEOUT = 30000
# Stealth Configuration
TYPING_WPM_MIN = 320
TYPING_WPM_MAX = 480
CLICK_DELAY_MIN_MS = 100
CLICK_DELAY_MAX_MS = 300
# Common Selectors
QUERY_INPUT_SELECTORS = [
"textarea.query-box-input",
'textarea[aria-label="Input for queries"]',
]
RESPONSE_SELECTORS = [
".to-user-container .message-text-content",
"[data-message-author='bot']",
"[data-message-author='assistant']",
]
# Artifact Selectors
ARTIFACT_SELECTORS = {
"slides": "button[aria-description='Slides']",
"audio": "button[aria-description='Audio Overview']",
"infographic": "button[aria-description='Infographic']",
"video": "button[aria-description='Video']",
}
# Customization Dialog Selectors
CUSTOMIZE_DIALOG = "mat-dialog-container:has-text('Customise')"
GENERATE_BUTTON = "mat-dialog-container button:has-text('Generate')"
"""
Download Handler for NotebookLM SuperSkill
Unified download management for all artifact types
"""
import time
from pathlib import Path
from typing import Optional, Dict, Any
from patchright.sync_api import Page, ElementHandle
from config import DOWNLOAD_TIMEOUT_MS
from stealth_utils import StealthUtils
class DownloadHandler:
"""Handle artifact downloads with consistent error handling"""
def __init__(self, page: Page, output_dir: Path):
"""
Initialize download handler.
Args:
page: Playwright page
output_dir: Directory to save downloads
"""
self.page = page
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
def download_artifact(
self,
artifact: ElementHandle,
prefix: str = "",
timeout: int = DOWNLOAD_TIMEOUT_MS
) -> Optional[Dict[str, Any]]:
"""
Download artifact via More menu > Download.
Args:
artifact: Artifact element to download
prefix: Prefix to add to filename
timeout: Download timeout in milliseconds
Returns:
Dict with download info, or None on failure
"""
try:
# Click More menu (may need to hover first)
more_btn = artifact.query_selector("button.artifact-more-button")
if not more_btn:
more_btn = artifact.query_selector("button[aria-label='More']")
if more_btn:
more_btn.click()
else:
# Try hovering to reveal the button
artifact.hover()
time.sleep(0.3)
more_btn = self.page.query_selector("button[aria-label='More']")
if more_btn:
more_btn.click()
else:
print(" More button not found")
return None
time.sleep(0.5)
# Click Download
download_btn = self.page.query_selector("button:has-text('Download')")
if not download_btn:
print(" Download button not found in menu")
return None
# Handle download with expect_download
with self.page.expect_download(timeout=timeout) as download_info:
download_btn.click()
download = download_info.value
# Build filename
suggested = download.suggested_filename or f"artifact_{int(time.time())}.pdf"
filename = self._format_filename(suggested, prefix)
save_path = self.output_dir / filename
# Save file
download.save_as(str(save_path))
print(f" Saved: {filename}")
return {
"filename": filename,
"path": str(save_path),
"suggested_filename": suggested,
}
except Exception as e:
print(f" Download error: {e}")
return None
def download_latest(
self,
artifact_selector: str,
prefix: str = ""
) -> Optional[Dict[str, Any]]:
"""
Download the most recent artifact matching selector.
Args:
artifact_selector: CSS selector for artifact type
prefix: Filename prefix
Returns:
Download info dict, or None
"""
artifacts = self.page.query_selector_all(artifact_selector)
if not artifacts:
print(" No artifacts found")
return None
latest = artifacts[-1]
return self.download_artifact(latest, prefix)
def download_all(
self,
artifact_selector: str,
prefix: str = "",
skip_loading: bool = True
) -> list:
"""
Download all artifacts matching selector.
Args:
artifact_selector: CSS selector for artifact type
prefix: Filename prefix
skip_loading: Skip artifacts still loading
Returns:
List of download info dicts
"""
from artifact_monitor import ArtifactMonitor
artifacts = self.page.query_selector_all(artifact_selector)
if not artifacts:
print(" No artifacts found")
return []
results = []
monitor = ArtifactMonitor(self.page)
for i, artifact in enumerate(artifacts):
# Skip if still loading
if skip_loading and monitor.is_shimmer_loading(artifact):
print(f" [{i+1}] Skipping (still loading)")
continue
indexed_prefix = f"{i+1:02d}_{prefix}" if prefix else f"{i+1:02d}"
result = self.download_artifact(artifact, indexed_prefix)
if result:
result["index"] = i + 1
results.append(result)
# Small delay between downloads
time.sleep(0.5)
return results
def _format_filename(self, suggested: str, prefix: str) -> str:
"""
Format filename with optional prefix.
Args:
suggested: Original suggested filename
prefix: Prefix to add
Returns:
Formatted filename
"""
if not prefix:
return suggested
# Split name and extension
if '.' in suggested:
name, ext = suggested.rsplit('.', 1)
else:
name, ext = suggested, 'pdf'
# Clean prefix (replace spaces with underscores)
clean_prefix = prefix.replace(' ', '_')
return f"{clean_prefix}_{name}.{ext}"
"""
Element Finder for NotebookLM SuperSkill
Multi-selector resilient element finding patterns
"""
from typing import Optional, List
from patchright.sync_api import Page, ElementHandle
def wait_and_find(
page: Page,
selectors: List[str],
timeout: int = 10,
description: str = "element",
state: str = "visible"
) -> Optional[ElementHandle]:
"""
Try multiple selectors until one works - resilient element finding.
This pattern handles UI changes gracefully by trying multiple
selector variants in order.
Args:
page: Playwright page
selectors: List of CSS selectors to try in order
timeout: Timeout per selector in seconds
description: Human-readable description for logging
state: Element state to wait for (visible, attached, etc.)
Returns:
First matching element, or None if all selectors fail
"""
for sel in selectors:
try:
el = page.wait_for_selector(sel, timeout=timeout * 1000, state=state)
if el:
print(f" Found {description}: {sel}")
return el
except Exception:
continue
print(f" Could not find {description}")
return None
def find_any(page: Page, selectors: List[str]) -> Optional[ElementHandle]:
"""
Find first matching element without waiting.
Args:
page: Playwright page
selectors: List of CSS selectors to try
Returns:
First matching element, or None
"""
for sel in selectors:
try:
el = page.query_selector(sel)
if el and el.is_visible():
return el
except Exception:
continue
return None
def find_all(page: Page, selector: str) -> List[ElementHandle]:
"""
Find all matching elements.
Args:
page: Playwright page
selector: CSS selector
Returns:
List of matching elements (may be empty)
"""
try:
return page.query_selector_all(selector)
except Exception:
return []
#!/usr/bin/env python3
"""
Audio Overview Generator for NotebookLM SuperSkill
Generate AI podcast-style audio overviews from NotebookLM notebooks
"""
import sys
import time
import argparse
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from patchright.sync_api import sync_playwright
from browser_factory import BrowserFactory
from element_finder import wait_and_find
from artifact_monitor import ArtifactMonitor
from download_handler import DownloadHandler
from config import AUDIO_TIMEOUT_SECONDS
# Audio format descriptions
AUDIO_FORMATS = {
"deep-dive": "Create a comprehensive deep dive podcast exploring all aspects of the content in detail.",
"brief": "Create a concise overview podcast summarizing the key points quickly.",
"critique": "Create a critical analysis podcast examining strengths, weaknesses, and implications.",
"debate": "Create a debate-style podcast with multiple perspectives and counterarguments.",
}
# Common language codes (subset of 80+ supported)
SUPPORTED_LANGUAGES = [
"en-US", "en-GB", "es-ES", "es-MX", "fr-FR", "de-DE",
"it-IT", "pt-BR", "pt-PT", "ja-JP", "ko-KR", "zh-CN",
"zh-TW", "ru-RU", "ar-SA", "hi-IN", "nl-NL", "pl-PL",
"sv-SE", "da-DK", "fi-FI", "no-NO", "tr-TR", "th-TH",
"vi-VN", "id-ID", "ms-MY", "tl-PH", "uk-UA", "cs-CZ",
]
def generate_audio_with_options(
page,
format_type: str = "deep-dive",
language: str = "en-US",
custom_prompt: str = None,
) -> bool:
"""
Generate audio overview using the customization dialog.
Args:
page: Playwright page
format_type: Audio format (deep-dive, brief, critique, debate)
language: Language code (e.g., en-US, es-ES)
custom_prompt: Custom instructions
Returns:
True if generation triggered successfully
"""
print(f"\nGenerating audio overview...")
print(f" Format: {format_type}, Language: {language}")
# Find Audio Overview in Studio panel
audio_selectors = [
"button[aria-label='Customise audio overview']",
"button.edit-button[data-edit-button-type='audio']",
".create-artifact-button-container[aria-label='Audio Overview'] button.edit-button",
"div[aria-label='Audio Overview'] button.edit-button",
]
edit_btn = None
for sel in audio_selectors:
try:
el = page.query_selector(sel)
if el and el.is_visible():
edit_btn = el
print(f" Found edit button: {sel}")
break
except:
continue
if not edit_btn:
# Try direct container approach
audio_container = page.query_selector("div[aria-label='Audio Overview']") or \
page.query_selector(".create-artifact-button-container:has-text('Audio Overview')")
if audio_container:
edit_btn = audio_container.query_selector("button.edit-button")
if not edit_btn:
# Fallback: click Audio Overview directly
audio_btn = page.query_selector("div[aria-label='Audio Overview']") or \
page.query_selector("button:has-text('Audio Overview')")
if audio_btn:
audio_btn.click()
time.sleep(1)
dialog = page.query_selector("mat-dialog-container")
if not dialog:
print(" Triggered direct generation")
return True
else:
print(" Could not find Audio Overview option")
return False
edit_btn.click()
time.sleep(1)
# Wait for dialog
dialog = page.query_selector("mat-dialog-container")
if not dialog:
print(" Dialog didn't open, generation may have started directly")
return True
print(" Customization dialog opened")
# Select format
format_labels = {
"deep-dive": "Deep dive",
"brief": "Brief",
"critique": "Critique",
"debate": "Debate",
}
format_label = format_labels.get(format_type, "Deep dive")
format_option = page.query_selector(f"mat-radio-button:has-text('{format_label}')")
if format_option:
format_option.click()
print(f" Selected format: {format_label}")
time.sleep(0.3)
# Select language
lang_dropdown = page.query_selector("mat-select[aria-label*='language']") or \
page.query_selector("mat-select:has-text('Language')")
if lang_dropdown:
lang_dropdown.click()
time.sleep(0.5)
lang_option = page.query_selector(f"mat-option:has-text('{language}')")
if lang_option:
lang_option.click()
print(f" Selected language: {language}")
time.sleep(0.3)
# Fill custom prompt if provided
if custom_prompt:
textarea = page.query_selector("mat-dialog-container textarea")
if textarea:
textarea.click()
textarea.fill("")
textarea.fill(custom_prompt)
print(" Filled custom prompt")
time.sleep(0.3)
# Click Generate
gen_btn = page.query_selector("mat-dialog-container button:has-text('Generate')") or \
page.query_selector("button:has-text('Generate')")
if gen_btn:
gen_btn.click()
print(" Clicked Generate!")
return True
else:
print(" Generate button not found")
return False
def generate_audio(
notebook_url: str,
output_dir: str = None,
format_type: str = "deep-dive",
language: str = "en-US",
custom_prompt: str = None,
headless: bool = True
) -> str:
"""
Generate an audio overview from a NotebookLM notebook.
Args:
notebook_url: NotebookLM notebook URL
output_dir: Output directory
format_type: Audio format (deep-dive, brief, critique, debate)
language: Language code
custom_prompt: Custom instructions
headless: Run in headless mode
Returns:
Path to downloaded audio file, or None on failure
"""
output_path = Path(output_dir) if output_dir else Path.cwd()
output_path.mkdir(parents=True, exist_ok=True)
print(f"Notebook: {notebook_url}")
print(f"Output: {output_path}")
print(f"Format: {format_type}")
print(f"Language: {language}")
print(f"Headless: {headless}\n")
playwright = sync_playwright().start()
context = BrowserFactory.launch_persistent_context(playwright, headless=headless)
page = context.new_page()
try:
# Navigate
print("1. Opening notebook...")
page.goto(notebook_url, wait_until="domcontentloaded")
time.sleep(4)
# Count initial audio artifacts
monitor = ArtifactMonitor(page, "audio")
initial_count = monitor.count_artifacts()
print(f" Initial audio count: {initial_count}")
# Trigger generation
print("\n2. Triggering audio generation...")
if not generate_audio_with_options(page, format_type, language, custom_prompt):
print("Failed to trigger generation")
return None
# Wait for audio (longer timeout - podcasts take 5-10 min)
print("\n3. Waiting for audio generation...")
print(" (This may take 5-10 minutes for podcasts)")
if not monitor.wait_for_artifact_ready(initial_count, AUDIO_TIMEOUT_SECONDS):
print("Timeout waiting for audio")
return None
# Download
print("\n4. Downloading audio...")
time.sleep(2)
downloader = DownloadHandler(page, output_path)
result = downloader.download_latest("button[aria-description='Audio Overview']", f"{format_type}_{language}")
if result:
print(f"\nSuccess! Saved to: {result['path']}")
return result['path']
else:
print("Download failed")
return None
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
return None
finally:
context.close()
playwright.stop()
def main():
parser = argparse.ArgumentParser(
description='Generate audio overviews (podcasts) from NotebookLM',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Deep dive podcast in English
python run.py generate_audio.py --notebook-url URL --format deep-dive
# Brief overview in Spanish
python run.py generate_audio.py --notebook-url URL --format brief --language es-ES
# Debate format with custom prompt
python run.py generate_audio.py --notebook-url URL --format debate --prompt "Focus on pros and cons"
Available formats:
deep-dive - Comprehensive exploration (10-15 min)
brief - Quick summary (3-5 min)
critique - Critical analysis
debate - Multiple perspectives
Common languages:
en-US, en-GB, es-ES, fr-FR, de-DE, it-IT, pt-BR, ja-JP, ko-KR, zh-CN
"""
)
parser.add_argument('--notebook-url', required=True, help='NotebookLM notebook URL')
parser.add_argument('--output', help='Output directory')
parser.add_argument('--format', choices=['deep-dive', 'brief', 'critique', 'debate'],
default='deep-dive', help='Audio format')
parser.add_argument('--language', default='en-US', help='Language code (e.g., en-US, es-ES)')
parser.add_argument('--prompt', help='Custom instructions')
parser.add_argument('--headless', action='store_true', help='Run in headless mode')
args = parser.parse_args()
result = generate_audio(
notebook_url=args.notebook_url,
output_dir=args.output,
format_type=args.format,
language=args.language,
custom_prompt=args.prompt,
headless=args.headless
)
sys.exit(0 if result else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Infographic Generator for NotebookLM SuperSkill
Generate visual infographics from NotebookLM notebooks
"""
import sys
import time
import argparse
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from patchright.sync_api import sync_playwright
from browser_factory import BrowserFactory
from element_finder import wait_and_find
from artifact_monitor import ArtifactMonitor
from download_handler import DownloadHandler
from config import ARTIFACT_TIMEOUT_SECONDS
# Orientation descriptions
ORIENTATIONS = {
"square": "1:1 aspect ratio - ideal for social media posts",
"portrait": "9:16 vertical - ideal for Instagram Stories, TikTok",
"landscape": "16:9 horizontal - ideal for LinkedIn, presentations",
}
# Detail levels
DETAIL_LEVELS = {
"concise": "High-level summary with key points only",
"standard": "Balanced detail with main concepts",
"detailed": "Comprehensive with supporting details",
}
def generate_infographic_with_options(
page,
orientation: str = "landscape",
detail: str = "standard",
custom_prompt: str = None,
) -> bool:
"""
Generate infographic using the customization dialog.
Args:
page: Playwright page
orientation: Aspect ratio (square, portrait, landscape)
detail: Detail level (concise, standard, detailed)
custom_prompt: Custom instructions
Returns:
True if generation triggered successfully
"""
print(f"\nGenerating infographic...")
print(f" Orientation: {orientation}, Detail: {detail}")
# Find Infographic in Studio panel
infographic_selectors = [
"button[aria-label='Customise infographic']",
"button.edit-button[data-edit-button-type='infographic']",
".create-artifact-button-container[aria-label='Infographic'] button.edit-button",
"div[aria-label='Infographic'] button.edit-button",
]
edit_btn = None
for sel in infographic_selectors:
try:
el = page.query_selector(sel)
if el and el.is_visible():
edit_btn = el
print(f" Found edit button: {sel}")
break
except:
continue
if not edit_btn:
# Try container approach
container = page.query_selector("div[aria-label='Infographic']") or \
page.query_selector(".create-artifact-button-container:has-text('Infographic')")
if container:
edit_btn = container.query_selector("button.edit-button")
if not edit_btn:
# Fallback: click Infographic directly
btn = page.query_selector("div[aria-label='Infographic']") or \
page.query_selector("button:has-text('Infographic')")
if btn:
btn.click()
time.sleep(1)
dialog = page.query_selector("mat-dialog-container")
if not dialog:
print(" Triggered direct generation")
return True
else:
print(" Could not find Infographic option")
return False
edit_btn.click()
time.sleep(1)
# Wait for dialog
dialog = page.query_selector("mat-dialog-container")
if not dialog:
print(" Dialog didn't open, generation may have started directly")
return True
print(" Customization dialog opened")
# Select orientation
orientation_labels = {
"square": "Square",
"portrait": "Portrait",
"landscape": "Landscape",
}
orientation_label = orientation_labels.get(orientation, "Landscape")
orientation_option = page.query_selector(f"mat-button-toggle:has-text('{orientation_label}')") or \
page.query_selector(f"mat-radio-button:has-text('{orientation_label}')")
if orientation_option:
orientation_option.click()
print(f" Selected orientation: {orientation_label}")
time.sleep(0.3)
# Select detail level
detail_labels = {
"concise": "Concise",
"standard": "Standard",
"detailed": "Detailed",
}
detail_label = detail_labels.get(detail, "Standard")
detail_option = page.query_selector(f"mat-button-toggle:has-text('{detail_label}')") or \
page.query_selector(f"mat-radio-button:has-text('{detail_label}')")
if detail_option:
detail_option.click()
print(f" Selected detail: {detail_label}")
time.sleep(0.3)
# Fill custom prompt if provided
if custom_prompt:
textarea = page.query_selector("mat-dialog-container textarea")
if textarea:
textarea.click()
textarea.fill("")
textarea.fill(custom_prompt)
print(" Filled custom prompt")
time.sleep(0.3)
# Click Generate
gen_btn = page.query_selector("mat-dialog-container button:has-text('Generate')") or \
page.query_selector("button:has-text('Generate')")
if gen_btn:
gen_btn.click()
print(" Clicked Generate!")
return True
else:
print(" Generate button not found")
return False
def generate_infographic(
notebook_url: str,
output_dir: str = None,
orientation: str = "landscape",
detail: str = "standard",
custom_prompt: str = None,
headless: bool = True
) -> str:
"""
Generate an infographic from a NotebookLM notebook.
Args:
notebook_url: NotebookLM notebook URL
output_dir: Output directory
orientation: Aspect ratio (square, portrait, landscape)
detail: Detail level (concise, standard, detailed)
custom_prompt: Custom instructions
headless: Run in headless mode
Returns:
Path to downloaded infographic, or None on failure
"""
output_path = Path(output_dir) if output_dir else Path.cwd()
output_path.mkdir(parents=True, exist_ok=True)
print(f"Notebook: {notebook_url}")
print(f"Output: {output_path}")
print(f"Orientation: {orientation}")
print(f"Detail: {detail}")
print(f"Headless: {headless}\n")
playwright = sync_playwright().start()
context = BrowserFactory.launch_persistent_context(playwright, headless=headless)
page = context.new_page()
try:
# Navigate
print("1. Opening notebook...")
page.goto(notebook_url, wait_until="domcontentloaded")
time.sleep(4)
# Count initial infographic artifacts
monitor = ArtifactMonitor(page, "infographic")
initial_count = monitor.count_artifacts()
print(f" Initial infographic count: {initial_count}")
# Trigger generation
print("\n2. Triggering infographic generation...")
if not generate_infographic_with_options(page, orientation, detail, custom_prompt):
print("Failed to trigger generation")
return None
# Wait for infographic
print("\n3. Waiting for infographic...")
if not monitor.wait_for_artifact_ready(initial_count, ARTIFACT_TIMEOUT_SECONDS):
print("Timeout waiting for infographic")
return None
# Download
print("\n4. Downloading infographic...")
time.sleep(2)
downloader = DownloadHandler(page, output_path)
result = downloader.download_latest("button[aria-description='Infographic']", f"{orientation}_{detail}")
if result:
print(f"\nSuccess! Saved to: {result['path']}")
return result['path']
else:
print("Download failed")
return None
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
return None
finally:
context.close()
playwright.stop()
def main():
parser = argparse.ArgumentParser(
description='Generate infographics from NotebookLM',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Landscape infographic with standard detail
python run.py generate_infographic.py --notebook-url URL
# Portrait for social media with concise detail
python run.py generate_infographic.py --notebook-url URL --orientation portrait --detail concise
# Square with custom prompt
python run.py generate_infographic.py --notebook-url URL --orientation square --prompt "Focus on statistics"
Orientations:
square - 1:1 ratio (social media posts)
portrait - 9:16 ratio (Instagram Stories, TikTok)
landscape - 16:9 ratio (LinkedIn, presentations)
Detail levels:
concise - High-level key points only
standard - Balanced detail
detailed - Comprehensive with supporting info
"""
)
parser.add_argument('--notebook-url', required=True, help='NotebookLM notebook URL')
parser.add_argument('--output', help='Output directory')
parser.add_argument('--orientation', choices=['square', 'portrait', 'landscape'],
default='landscape', help='Aspect ratio')
parser.add_argument('--detail', choices=['concise', 'standard', 'detailed'],
default='standard', help='Detail level')
parser.add_argument('--prompt', help='Custom instructions')
parser.add_argument('--headless', action='store_true', help='Run in headless mode')
args = parser.parse_args()
result = generate_infographic(
notebook_url=args.notebook_url,
output_dir=args.output,
orientation=args.orientation,
detail=args.detail,
custom_prompt=args.prompt,
headless=args.headless
)
sys.exit(0 if result else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Slide Deck Generator for NotebookLM SuperSkill
Generate customized slide decks from NotebookLM notebooks
"""
import sys
import time
import argparse
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from patchright.sync_api import sync_playwright
from browser_factory import BrowserFactory
from element_finder import wait_and_find
from artifact_monitor import ArtifactMonitor
from download_handler import DownloadHandler
from stealth_utils import StealthUtils
from config import ARTIFACT_TIMEOUT_SECONDS
# Predefined audience prompts
AUDIENCE_PROMPTS = {
"technical": "Create a detailed technical presentation for software engineers and developers. Focus on architecture, implementation details, technical specifications, and code examples. Use precise technical terminology.",
"investor": "Create a compelling investor pitch deck. Focus on market opportunity, business model, competitive advantages, financial projections, and growth potential. Use clear metrics and persuasive language.",
"customer": "Create a customer-facing presentation that highlights benefits and value proposition. Focus on how the product solves problems, ease of use, and success stories. Use simple, accessible language.",
"executive": "Create an executive summary presentation for C-level decision makers. Focus on strategic value, ROI, key metrics, and high-level roadmap. Be concise and business-focused.",
"beginner": "Create an introductory presentation for beginners new to the topic. Focus on fundamentals, step-by-step explanations, and simple examples. Use clear, educational language.",
}
def upload_source(page, source_path: Path) -> bool:
"""Upload a source file to the current notebook"""
print("Uploading source file...")
upload_selectors = [
"button[aria-label='Upload sources from your computer']",
"button[aria-label='Opens the upload source dialogue']",
"button:has-text('Upload a source')",
]
upload_btn = wait_and_find(page, upload_selectors, timeout=5, description="upload button")
if upload_btn:
upload_btn.click()
time.sleep(1)
file_input = page.query_selector("input[type='file']")
if file_input:
file_input.set_input_files(str(source_path))
print(f" File selected: {source_path.name}")
else:
print(" No file input found")
return False
# Wait for upload to complete
print(" Processing source...")
time.sleep(5)
for i in range(30):
modal = page.query_selector("[role='dialog']:has-text('Add sources')")
if not modal or not modal.is_visible():
print(" Source added!")
return True
time.sleep(1)
if i % 10 == 0 and i > 0:
print(f" Still processing... ({i}s)")
return True
def generate_slide_with_options(
page,
prompt: str = "",
format_type: str = "detailed",
length: str = "default",
) -> bool:
"""
Generate slides using the customization dialog.
Args:
page: Playwright page
prompt: Custom prompt for slide content
format_type: "detailed" or "presenter"
length: "short", "default", or "long"
Returns:
True if generation triggered successfully
"""
print(f"\nGenerating slides...")
print(f" Format: {format_type}, Length: {length}")
if prompt:
print(f" Prompt: {prompt[:50]}...")
# Find and click the Edit button on Slide Deck
edit_btn_selectors = [
"button[aria-label='Customise slide deck']",
"button.edit-button[data-edit-button-type='8']",
".create-artifact-button-container[aria-label='Slide deck'] button.edit-button",
"div[aria-label='Slide deck'] button.edit-button",
]
edit_btn = None
for sel in edit_btn_selectors:
try:
el = page.query_selector(sel)
if el and el.is_visible():
edit_btn = el
print(f" Found edit button: {sel}")
break
except:
continue
if not edit_btn:
# Try container approach
slide_container = page.query_selector("div.create-artifact-button-container[aria-label='Slide deck']") or \
page.query_selector("div[aria-label='Slide deck']")
if slide_container:
edit_btn = slide_container.query_selector("button.edit-button")
if edit_btn:
print(" Found edit button via container")
if not edit_btn:
print(" Edit button not found, trying direct click...")
slide_btn = page.query_selector("div[aria-label='Slide deck']")
if slide_btn:
slide_btn.click()
time.sleep(1)
dialog = page.query_selector("mat-dialog-container:has-text('Customise')")
if not dialog:
return True
else:
print(" Could not find Slide Deck option")
return False
edit_btn.click()
time.sleep(1)
# Wait for dialog
dialog = page.wait_for_selector("mat-dialog-container:has-text('Customise')", timeout=5000)
if not dialog:
print(" Customization dialog didn't open")
return False
print(" Customization dialog opened")
# Set Format
if format_type == "presenter":
presenter_radio = page.query_selector("mat-radio-button:has-text('Presenter slides')")
if presenter_radio:
presenter_radio.click()
print(" Selected: Presenter slides")
time.sleep(0.3)
else:
detailed_radio = page.query_selector("mat-radio-button:has-text('Detailed deck')")
if detailed_radio:
detailed_radio.click()
print(" Selected: Detailed deck")
time.sleep(0.3)
# Set Length
length_map = {"short": "Short", "default": "Default", "long": "Long"}
length_label = length_map.get(length, "Default")
length_btn = page.query_selector(f"mat-button-toggle:has-text('{length_label}')")
if length_btn:
length_btn.click()
print(f" Selected length: {length_label}")
time.sleep(0.3)
# Set Prompt
if prompt:
textarea = page.query_selector("mat-dialog-container textarea")
if textarea:
textarea.click()
textarea.fill("")
textarea.fill(prompt)
print(" Filled prompt")
time.sleep(0.3)
# Click Generate
gen_btn = page.query_selector("mat-dialog-container button:has-text('Generate')")
if gen_btn:
gen_btn.click()
print(" Clicked Generate!")
return True
else:
print(" Generate button not found")
return False
def generate_slides(
notebook_url: str,
source_file: str = None,
output_dir: str = None,
audience: str = "technical",
format_type: str = "detailed",
length: str = "default",
custom_prompt: str = None,
headless: bool = True
) -> str:
"""
Generate a slide deck from a NotebookLM notebook.
Args:
notebook_url: NotebookLM notebook URL
source_file: Optional source file to upload
output_dir: Output directory for slides
audience: Audience type (technical, investor, customer, executive, beginner)
format_type: Slide format (detailed, presenter)
length: Slide length (short, default, long)
custom_prompt: Custom prompt (overrides audience)
headless: Run in headless mode
Returns:
Path to downloaded slides, or None on failure
"""
output_path = Path(output_dir) if output_dir else Path.cwd()
output_path.mkdir(parents=True, exist_ok=True)
# Use custom prompt or audience prompt
prompt = custom_prompt if custom_prompt else AUDIENCE_PROMPTS.get(audience, audience)
print(f"Notebook: {notebook_url}")
print(f"Output: {output_path}")
print(f"Audience: {audience}")
print(f"Headless: {headless}\n")
playwright = sync_playwright().start()
context = BrowserFactory.launch_persistent_context(playwright, headless=headless)
page = context.new_page()
try:
# Navigate to notebook
print("1. Opening notebook...")
page.goto(notebook_url, wait_until="domcontentloaded")
time.sleep(4)
# Upload source if provided
if source_file:
source_path = Path(source_file).absolute()
if not source_path.exists():
print(f"File not found: {source_file}")
return None
modal = page.query_selector("[role='dialog']:has-text('Add sources')")
if modal and modal.is_visible():
print("2. Uploading source...")
if not upload_source(page, source_path):
print("Failed to upload source")
return None
time.sleep(3)
else:
print("2. Notebook already has sources")
else:
print("2. Using existing sources")
# Count initial artifacts
monitor = ArtifactMonitor(page, "slides")
initial_count = monitor.count_artifacts()
print(f" Initial slide count: {initial_count}")
# Trigger generation
print("\n3. Triggering slide generation...")
if not generate_slide_with_options(page, prompt, format_type, length):
print("Failed to trigger generation")
return None
# Wait for slides to be ready
print("\n4. Waiting for slides...")
if not monitor.wait_for_artifact_ready(initial_count, ARTIFACT_TIMEOUT_SECONDS):
print("Timeout waiting for slides")
return None
# Download
print("\n5. Downloading slides...")
time.sleep(1)
downloader = DownloadHandler(page, output_path)
result = downloader.download_latest("button[aria-description='Slides']", audience)
if result:
print(f"\nSuccess! Saved to: {result['path']}")
return result['path']
else:
print("Download failed")
return None
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
return None
finally:
context.close()
playwright.stop()
def generate_multiple_slides(
notebook_url: str,
source_file: str = None,
output_dir: str = None,
audiences: list = None,
format_type: str = "detailed",
length: str = "default",
headless: bool = True
) -> list:
"""
Generate multiple slide decks for different audiences.
Uses 3-phase approach:
1. Trigger all generations quickly
2. Wait for all to complete
3. Download all slides
Args:
notebook_url: NotebookLM notebook URL
source_file: Optional source file
output_dir: Output directory
audiences: List of audience types
format_type: Slide format
length: Slide length
headless: Run in headless mode
Returns:
List of downloaded file paths
"""
if not audiences:
audiences = ["technical", "investor", "customer"]
output_path = Path(output_dir) if output_dir else Path.cwd()
output_path.mkdir(parents=True, exist_ok=True)
print(f"Notebook: {notebook_url}")
print(f"Output: {output_path}")
print(f"Audiences: {', '.join(audiences)}\n")
playwright = sync_playwright().start()
context = BrowserFactory.launch_persistent_context(playwright, headless=headless)
page = context.new_page()
results = []
try:
# Navigate
print("1. Opening notebook...")
page.goto(notebook_url, wait_until="domcontentloaded")
time.sleep(4)
# Upload source if provided
if source_file:
source_path = Path(source_file).absolute()
if source_path.exists():
modal = page.query_selector("[role='dialog']:has-text('Add sources')")
if modal and modal.is_visible():
print("2. Uploading source...")
upload_source(page, source_path)
time.sleep(3)
# Count initial artifacts
monitor = ArtifactMonitor(page, "slides")
initial_count = monitor.count_artifacts()
print(f" Initial slide count: {initial_count}")
# PHASE 1: Trigger all generations
print(f"\n{'='*50}")
print(f"PHASE 1: TRIGGERING {len(audiences)} GENERATIONS")
print('='*50)
triggered = []
for i, audience in enumerate(audiences, 1):
print(f"\n[{i}/{len(audiences)}] Triggering: {audience.upper()}")
prompt = AUDIENCE_PROMPTS.get(audience, audience)
if generate_slide_with_options(page, prompt, format_type, length):
triggered.append(audience)
print(f" {audience} triggered!")
else:
print(f" {audience}: Failed to trigger")
time.sleep(1)
print(f"\nTriggered {len(triggered)}/{len(audiences)} generations")
# PHASE 2: Wait for all slides
print(f"\n{'='*50}")
print("PHASE 2: WAITING FOR COMPLETION")
print('='*50)
expected_count = initial_count + len(triggered)
monitor.wait_for_multiple_ready(expected_count, ARTIFACT_TIMEOUT_SECONDS * 2)
time.sleep(2)
# PHASE 3: Download all
print(f"\n{'='*50}")
print("PHASE 3: DOWNLOADING")
print('='*50)
artifacts = page.query_selector_all("button[aria-description='Slides']")
new_artifacts = artifacts[initial_count:]
for i, (audience, artifact) in enumerate(zip(triggered, new_artifacts)):
print(f"\n[{i+1}/{len(new_artifacts)}] Downloading: {audience}")
try:
downloader = DownloadHandler(page, output_path)
result = downloader.download_artifact(artifact, audience)
if result:
results.append(result['path'])
except Exception as e:
print(f" Error: {e}")
time.sleep(0.5)
print(f"\n{'='*50}")
print(f"COMPLETED: {len(results)}/{len(audiences)} slide decks")
print('='*50)
for r in results:
print(f" {Path(r).name}")
return results
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
return results
finally:
context.close()
playwright.stop()
def main():
parser = argparse.ArgumentParser(
description='Generate slide decks from NotebookLM',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Single slide deck
python run.py generate_slides.py --notebook-url URL --audience technical
# Multiple audiences
python run.py generate_slides.py --notebook-url URL --audiences technical,investor,customer
# With source file
python run.py generate_slides.py --notebook-url URL --source spec.md --audience investor
Available audience types:
technical - For developers (architecture, code, specs)
investor - For investors (market, financials, growth)
customer - For customers (benefits, value, ease of use)
executive - For C-level (strategy, ROI, metrics)
beginner - For newcomers (fundamentals, step-by-step)
"""
)
parser.add_argument('--notebook-url', required=True, help='NotebookLM notebook URL')
parser.add_argument('--source', help='Path to source file to upload')
parser.add_argument('--output', help='Output directory')
parser.add_argument('--audience', default='technical', help='Audience type')
parser.add_argument('--audiences', help='Comma-separated audience types for batch generation')
parser.add_argument('--format', choices=['detailed', 'presenter'], default='detailed', help='Slide format')
parser.add_argument('--length', choices=['short', 'default', 'long'], default='default', help='Slide length')
parser.add_argument('--prompt', help='Custom prompt (overrides audience)')
parser.add_argument('--headless', action='store_true', help='Run in headless mode')
args = parser.parse_args()
if args.audiences:
audiences = [a.strip() for a in args.audiences.split(',')]
results = generate_multiple_slides(
notebook_url=args.notebook_url,
source_file=args.source,
output_dir=args.output,
audiences=audiences,
format_type=args.format,
length=args.length,
headless=args.headless
)
sys.exit(0 if results else 1)
else:
result = generate_slides(
notebook_url=args.notebook_url,
source_file=args.source,
output_dir=args.output,
audience=args.audience,
format_type=args.format,
length=args.length,
custom_prompt=args.prompt,
headless=args.headless
)
sys.exit(0 if result else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Video Overview Generator for NotebookLM SuperSkill
Generate video overviews from NotebookLM notebooks
"""
import sys
import time
import argparse
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from patchright.sync_api import sync_playwright
from browser_factory import BrowserFactory
from element_finder import wait_and_find
from artifact_monitor import ArtifactMonitor
from download_handler import DownloadHandler
from config import VIDEO_TIMEOUT_SECONDS
# Video format descriptions
VIDEO_FORMATS = {
"brief": "Short summary video (1-2 minutes) - quick overview",
"explainer": "Detailed explainer video (3-5 minutes) - structured walkthrough",
}
# Visual themes
VIDEO_THEMES = {
"retro-90s": "Nostalgic 90s aesthetic with bold colors and retro graphics",
"futuristic": "Modern sci-fi inspired with sleek animations",
"corporate": "Professional business style with clean design",
"minimal": "Clean minimalist design with subtle animations",
}
def generate_video_with_options(
page,
format_type: str = "brief",
theme: str = "corporate",
custom_theme: str = None,
custom_prompt: str = None,
) -> bool:
"""
Generate video overview using the customization dialog.
Args:
page: Playwright page
format_type: Video format (brief, explainer)
theme: Visual theme (retro-90s, futuristic, corporate, minimal)
custom_theme: Custom theme description
custom_prompt: Custom instructions
Returns:
True if generation triggered successfully
"""
print(f"\nGenerating video overview...")
print(f" Format: {format_type}, Theme: {theme}")
# Find Video in Studio panel
video_selectors = [
"button[aria-label='Customise video overview']",
"button.edit-button[data-edit-button-type='video']",
".create-artifact-button-container[aria-label='Video'] button.edit-button",
"div[aria-label='Video'] button.edit-button",
]
edit_btn = None
for sel in video_selectors:
try:
el = page.query_selector(sel)
if el and el.is_visible():
edit_btn = el
print(f" Found edit button: {sel}")
break
except:
continue
if not edit_btn:
# Try container approach
container = page.query_selector("div[aria-label='Video']") or \
page.query_selector(".create-artifact-button-container:has-text('Video')")
if container:
edit_btn = container.query_selector("button.edit-button")
if not edit_btn:
# Fallback: click Video directly
btn = page.query_selector("div[aria-label='Video']") or \
page.query_selector("button:has-text('Video')")
if btn:
btn.click()
time.sleep(1)
dialog = page.query_selector("mat-dialog-container")
if not dialog:
print(" Triggered direct generation")
return True
else:
print(" Could not find Video option")
return False
edit_btn.click()
time.sleep(1)
# Wait for dialog
dialog = page.query_selector("mat-dialog-container")
if not dialog:
print(" Dialog didn't open, generation may have started directly")
return True
print(" Customization dialog opened")
# Select format
format_labels = {
"brief": "Brief",
"explainer": "Explainer",
}
format_label = format_labels.get(format_type, "Brief")
format_option = page.query_selector(f"mat-radio-button:has-text('{format_label}')") or \
page.query_selector(f"mat-button-toggle:has-text('{format_label}')")
if format_option:
format_option.click()
print(f" Selected format: {format_label}")
time.sleep(0.3)
# Select theme
if custom_theme:
# Use custom theme input
theme_input = page.query_selector("mat-dialog-container input[placeholder*='theme']") or \
page.query_selector("mat-dialog-container textarea")
if theme_input:
theme_input.click()
theme_input.fill("")
theme_input.fill(custom_theme)
print(f" Set custom theme: {custom_theme[:30]}...")
time.sleep(0.3)
else:
theme_labels = {
"retro-90s": "Retro 90s",
"futuristic": "Futuristic",
"corporate": "Corporate",
"minimal": "Minimal",
}
theme_label = theme_labels.get(theme, "Corporate")
theme_option = page.query_selector(f"mat-radio-button:has-text('{theme_label}')") or \
page.query_selector(f"mat-button-toggle:has-text('{theme_label}')")
if theme_option:
theme_option.click()
print(f" Selected theme: {theme_label}")
time.sleep(0.3)
# Fill custom prompt if provided
if custom_prompt:
textarea = page.query_selector("mat-dialog-container textarea:not([placeholder*='theme'])")
if textarea:
textarea.click()
textarea.fill("")
textarea.fill(custom_prompt)
print(" Filled custom prompt")
time.sleep(0.3)
# Click Generate
gen_btn = page.query_selector("mat-dialog-container button:has-text('Generate')") or \
page.query_selector("button:has-text('Generate')")
if gen_btn:
gen_btn.click()
print(" Clicked Generate!")
return True
else:
print(" Generate button not found")
return False
def generate_video(
notebook_url: str,
output_dir: str = None,
format_type: str = "brief",
theme: str = "corporate",
custom_theme: str = None,
custom_prompt: str = None,
headless: bool = True
) -> str:
"""
Generate a video overview from a NotebookLM notebook.
Args:
notebook_url: NotebookLM notebook URL
output_dir: Output directory
format_type: Video format (brief, explainer)
theme: Visual theme
custom_theme: Custom theme description
custom_prompt: Custom instructions
headless: Run in headless mode
Returns:
Path to downloaded video, or None on failure
"""
output_path = Path(output_dir) if output_dir else Path.cwd()
output_path.mkdir(parents=True, exist_ok=True)
print(f"Notebook: {notebook_url}")
print(f"Output: {output_path}")
print(f"Format: {format_type}")
print(f"Theme: {custom_theme if custom_theme else theme}")
print(f"Headless: {headless}\n")
playwright = sync_playwright().start()
context = BrowserFactory.launch_persistent_context(playwright, headless=headless)
page = context.new_page()
try:
# Navigate
print("1. Opening notebook...")
page.goto(notebook_url, wait_until="domcontentloaded")
time.sleep(4)
# Count initial video artifacts
monitor = ArtifactMonitor(page, "video")
initial_count = monitor.count_artifacts()
print(f" Initial video count: {initial_count}")
# Trigger generation
print("\n2. Triggering video generation...")
if not generate_video_with_options(page, format_type, theme, custom_theme, custom_prompt):
print("Failed to trigger generation")
return None
# Wait for video (longest timeout - videos take 10+ min)
print("\n3. Waiting for video generation...")
print(" (This may take 10-15 minutes for videos)")
if not monitor.wait_for_artifact_ready(initial_count, VIDEO_TIMEOUT_SECONDS):
print("Timeout waiting for video")
return None
# Download
print("\n4. Downloading video...")
time.sleep(2)
downloader = DownloadHandler(page, output_path)
prefix = f"{format_type}_{theme}" if not custom_theme else f"{format_type}_custom"
result = downloader.download_latest("button[aria-description='Video']", prefix)
if result:
print(f"\nSuccess! Saved to: {result['path']}")
return result['path']
else:
print("Download failed")
return None
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
return None
finally:
context.close()
playwright.stop()
def main():
parser = argparse.ArgumentParser(
description='Generate video overviews from NotebookLM',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Brief video with corporate theme
python run.py generate_video.py --notebook-url URL
# Explainer video with futuristic theme
python run.py generate_video.py --notebook-url URL --format explainer --theme futuristic
# Video with custom theme
python run.py generate_video.py --notebook-url URL --custom-theme "Minimalist with pastel colors"
Formats:
brief - Short summary (1-2 min)
explainer - Detailed walkthrough (3-5 min)
Themes:
retro-90s - Bold colors, retro graphics
futuristic - Sleek, sci-fi inspired
corporate - Clean, professional
minimal - Subtle, minimalist
"""
)
parser.add_argument('--notebook-url', required=True, help='NotebookLM notebook URL')
parser.add_argument('--output', help='Output directory')
parser.add_argument('--format', choices=['brief', 'explainer'],
default='brief', help='Video format')
parser.add_argument('--theme', choices=['retro-90s', 'futuristic', 'corporate', 'minimal'],
default='corporate', help='Visual theme')
parser.add_argument('--custom-theme', help='Custom theme description')
parser.add_argument('--prompt', help='Custom instructions')
parser.add_argument('--headless', action='store_true', help='Run in headless mode')
args = parser.parse_args()
result = generate_video(
notebook_url=args.notebook_url,
output_dir=args.output,
format_type=args.format,
theme=args.theme,
custom_theme=args.custom_theme,
custom_prompt=args.prompt,
headless=args.headless
)
sys.exit(0 if result else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Universal runner for NotebookLM SuperSkill scripts
Ensures all scripts run with the correct virtual environment
"""
import os
import sys
import subprocess
from pathlib import Path
def get_venv_python():
"""Get the virtual environment Python executable"""
skill_dir = Path(__file__).parent.parent
venv_dir = skill_dir / ".venv"
if os.name == 'nt': # Windows
venv_python = venv_dir / "Scripts" / "python.exe"
else: # Unix/Linux/Mac
venv_python = venv_dir / "bin" / "python"
return venv_python
def ensure_venv():
"""Ensure virtual environment exists"""
skill_dir = Path(__file__).parent.parent
venv_dir = skill_dir / ".venv"
setup_script = skill_dir / "scripts" / "setup_environment.py"
if not venv_dir.exists():
print("First-time setup: Creating virtual environment...")
print(" This may take a minute...")
result = subprocess.run([sys.executable, str(setup_script)])
if result.returncode != 0:
print("Failed to set up environment")
sys.exit(1)
print("Environment ready!")
return get_venv_python()
def main():
"""Main runner"""
if len(sys.argv) < 2:
print("Usage: python run.py <script_name> [args...]")
print("\nAvailable scripts:")
print(" auth_manager.py - Handle authentication")
print(" generate_slides.py - Generate slide decks")
print(" generate_audio.py - Generate audio overviews (podcasts)")
print(" generate_infographic.py - Generate infographics")
print(" generate_video.py - Generate video overviews")
sys.exit(1)
script_name = sys.argv[1]
script_args = sys.argv[2:]
# Handle both "scripts/script.py" and "script.py" formats
if script_name.startswith('scripts/'):
script_name = script_name[8:]
# Ensure .py extension
if not script_name.endswith('.py'):
script_name += '.py'
# Get script path
skill_dir = Path(__file__).parent.parent
script_path = skill_dir / "scripts" / script_name
if not script_path.exists():
print(f"Script not found: {script_name}")
print(f" Looked for: {script_path}")
sys.exit(1)
# Ensure venv exists and get Python executable
venv_python = ensure_venv()
# Build command
cmd = [str(venv_python), str(script_path)] + script_args
# Run the script
try:
result = subprocess.run(cmd)
sys.exit(result.returncode)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(130)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Environment Setup for NotebookLM SuperSkill
Manages virtual environment and dependencies automatically
"""
import os
import sys
import subprocess
import venv
from pathlib import Path
class SkillEnvironment:
"""Manages skill-specific virtual environment"""
def __init__(self):
self.skill_dir = Path(__file__).parent.parent
self.venv_dir = self.skill_dir / ".venv"
self.requirements_file = self.skill_dir / "requirements.txt"
if os.name == 'nt': # Windows
self.venv_python = self.venv_dir / "Scripts" / "python.exe"
self.venv_pip = self.venv_dir / "Scripts" / "pip.exe"
else: # Unix/Linux/Mac
self.venv_python = self.venv_dir / "bin" / "python"
self.venv_pip = self.venv_dir / "bin" / "pip"
def ensure_venv(self) -> bool:
"""Ensure virtual environment exists and is set up"""
if self.is_in_skill_venv():
print("Already running in skill virtual environment")
return True
# Create venv if it doesn't exist
if not self.venv_dir.exists():
print(f"Creating virtual environment in {self.venv_dir.name}/")
try:
venv.create(self.venv_dir, with_pip=True)
print("Virtual environment created")
except Exception as e:
print(f"Failed to create venv: {e}")
return False
# Install/update dependencies
if self.requirements_file.exists():
print("Installing dependencies...")
try:
# Upgrade pip first
subprocess.run(
[str(self.venv_pip), "install", "--upgrade", "pip"],
check=True,
capture_output=True,
text=True
)
# Install requirements
subprocess.run(
[str(self.venv_pip), "install", "-r", str(self.requirements_file)],
check=True,
capture_output=True,
text=True
)
print("Dependencies installed")
# Install Chrome for Patchright (real Chrome, not Chromium)
print("Installing Google Chrome for Patchright...")
try:
subprocess.run(
[str(self.venv_python), "-m", "patchright", "install", "chrome"],
check=True,
capture_output=True,
text=True
)
print("Chrome installed")
except subprocess.CalledProcessError as e:
print(f"Warning: Failed to install Chrome: {e}")
print(" You may need to run manually: python -m patchright install chrome")
return True
except subprocess.CalledProcessError as e:
print(f"Failed to install dependencies: {e}")
return False
else:
print("No requirements.txt found")
return True
def is_in_skill_venv(self) -> bool:
"""Check if we're already running in the skill's venv"""
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
venv_path = Path(sys.prefix)
return venv_path == self.venv_dir
return False
def get_python_executable(self) -> str:
"""Get the correct Python executable to use"""
if self.venv_python.exists():
return str(self.venv_python)
return sys.executable
def activate_instructions(self) -> str:
"""Get instructions for manual activation"""
if os.name == 'nt':
activate = self.venv_dir / "Scripts" / "activate.bat"
return f"Run: {activate}"
else:
activate = self.venv_dir / "bin" / "activate"
return f"Run: source {activate}"
def main():
"""Main entry point for environment setup"""
import argparse
parser = argparse.ArgumentParser(description='Setup NotebookLM SuperSkill environment')
parser.add_argument('--check', action='store_true', help='Check if environment is set up')
args = parser.parse_args()
env = SkillEnvironment()
if args.check:
if env.venv_dir.exists():
print(f"Virtual environment exists: {env.venv_dir}")
print(f" Python: {env.get_python_executable()}")
print(f" To activate manually: {env.activate_instructions()}")
else:
print("No virtual environment found")
print(" Run setup_environment.py to create it")
return
# Default: ensure environment is set up
if env.ensure_venv():
print("\nEnvironment ready!")
print(f" Virtual env: {env.venv_dir}")
print(f" Python: {env.get_python_executable()}")
print(f"\nTo activate manually: {env.activate_instructions()}")
else:
print("\nEnvironment setup failed")
return 1
if __name__ == "__main__":
sys.exit(main() or 0)
"""
Stealth Utilities for NotebookLM SuperSkill
Human-like interaction patterns for anti-detection
"""
import time
import random
from typing import Optional
from patchright.sync_api import Page, ElementHandle
from config import (
TYPING_WPM_MIN, TYPING_WPM_MAX,
CLICK_DELAY_MIN_MS, CLICK_DELAY_MAX_MS
)
class StealthUtils:
"""Human-like interaction utilities for anti-detection"""
@staticmethod
def random_delay(min_ms: int = 100, max_ms: int = 500):
"""Add random delay to simulate human timing"""
time.sleep(random.uniform(min_ms / 1000, max_ms / 1000))
@staticmethod
def human_type(
page: Page,
selector: str,
text: str,
wpm_min: int = TYPING_WPM_MIN,
wpm_max: int = TYPING_WPM_MAX
):
"""
Type with human-like variable speed and occasional pauses.
Args:
page: Playwright page
selector: CSS selector for input element
text: Text to type
wpm_min: Minimum typing speed (words per minute)
wpm_max: Maximum typing speed (words per minute)
"""
element = page.query_selector(selector)
if not element:
try:
element = page.wait_for_selector(selector, timeout=2000)
except Exception:
pass
if not element:
print(f" Element not found for typing: {selector}")
return
# Click to focus
element.click()
# Type character by character with variable speed
for char in text:
element.type(char, delay=random.uniform(25, 75))
# Occasional longer pause (5% chance)
if random.random() < 0.05:
time.sleep(random.uniform(0.15, 0.4))
@staticmethod
def realistic_click(page: Page, selector: str) -> bool:
"""
Click with realistic mouse movement.
Args:
page: Playwright page
selector: CSS selector for element
Returns:
True if click succeeded
"""
element = page.query_selector(selector)
if not element:
return False
# Move mouse to element with smooth motion
box = element.bounding_box()
if box:
x = box['x'] + box['width'] / 2
y = box['y'] + box['height'] / 2
page.mouse.move(x, y, steps=5)
StealthUtils.random_delay(CLICK_DELAY_MIN_MS, CLICK_DELAY_MAX_MS)
element.click()
StealthUtils.random_delay(CLICK_DELAY_MIN_MS, CLICK_DELAY_MAX_MS)
return True
@staticmethod
def fill_with_delay(element: ElementHandle, text: str, clear: bool = True):
"""
Fill an input with text after a small delay.
Args:
element: Element to fill
text: Text to enter
clear: Clear existing content first
"""
element.click()
StealthUtils.random_delay(50, 150)
if clear:
element.fill("")
StealthUtils.random_delay(50, 100)
element.fill(text)
StealthUtils.random_delay(100, 200)
Related skills
FAQ
What formats can it generate?
Slide decks, audio podcasts, infographics, and video overviews, each with format and audience options.
Does it need authentication?
Yes, a one-time Google login via auth_manager.py before first use.