Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
- name: Verify extension files are present
run: |
echo "🔍 Verifying extension files are included..."

# Check required extension files exist
REQUIRED_FILES=(
"sentience/extension/manifest.json"
Expand All @@ -63,14 +63,14 @@ jobs:
"sentience/extension/pkg/sentience_core.js"
"sentience/extension/pkg/sentience_core_bg.wasm"
)

MISSING_FILES=()
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$file" ]; then
MISSING_FILES+=("$file")
fi
done

if [ ${#MISSING_FILES[@]} -ne 0 ]; then
echo "❌ Error: Missing required extension files:"
printf ' - %s\n' "${MISSING_FILES[@]}"
Expand All @@ -79,14 +79,14 @@ jobs:
echo "Run the sync-extension workflow or manually sync extension files."
exit 1
fi

# Verify findTextRect function exists in injected_api.js
if ! grep -q "findTextRect:" sentience/extension/injected_api.js; then
echo "❌ Error: findTextRect function not found in injected_api.js"
echo "The extension may be out of date. Please sync the extension before releasing."
exit 1
fi

echo "✅ All extension files verified"
echo "📦 Extension files that will be included:"
find sentience/extension -type f | sort
Expand All @@ -98,38 +98,38 @@ jobs:
- name: Check package
run: |
twine check dist/*

- name: Verify extension files in built package
run: |
echo "🔍 Verifying extension files are included in the built package..."

# Extract wheel to check contents
WHEEL_FILE=$(ls dist/*.whl | head -1)
WHEEL_PATH=$(realpath "$WHEEL_FILE")
echo "Checking wheel: $WHEEL_PATH"

# Create temp directory for extraction
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"

# Extract wheel (it's a zip file)
unzip -q "$WHEEL_PATH"

# Check for required extension files in the wheel
REQUIRED_IN_WHEEL=(
"sentience/extension/manifest.json"
"sentience/extension/injected_api.js"
"sentience/extension/pkg/sentience_core.js"
"sentience/extension/pkg/sentience_core_bg.wasm"
)

MISSING_IN_WHEEL=()
for file in "${REQUIRED_IN_WHEEL[@]}"; do
if [ ! -f "$file" ]; then
MISSING_IN_WHEEL+=("$file")
fi
done

if [ ${#MISSING_IN_WHEEL[@]} -ne 0 ]; then
echo "❌ Error: Extension files missing from built wheel:"
printf ' - %s\n' "${MISSING_IN_WHEEL[@]}"
Expand All @@ -138,17 +138,17 @@ jobs:
echo "Check MANIFEST.in and pyproject.toml package-data settings."
exit 1
fi

# Verify findTextRect is in the packaged injected_api.js
if ! grep -q "findTextRect:" sentience/extension/injected_api.js; then
echo "❌ Error: findTextRect not found in packaged injected_api.js"
exit 1
fi

echo "✅ All extension files verified in built package"
echo "📦 Extension files found in wheel:"
find sentience/extension -type f | sort

# Cleanup
rm -rf "$TEMP_DIR"

Expand Down
91 changes: 91 additions & 0 deletions examples/video_recording_advanced.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Advanced Video Recording Demo

Demonstrates advanced video recording features:
- Custom resolution (1080p)
- Custom output filename
- Multiple recordings in one session
"""

from datetime import datetime
from pathlib import Path

from sentience import SentienceBrowser


def main():
print("\n" + "=" * 60)
print("Advanced Video Recording Demo")
print("=" * 60 + "\n")

video_dir = Path("./recordings")
video_dir.mkdir(exist_ok=True)

# Example 1: Custom Resolution (1080p)
print("📹 Example 1: Recording in 1080p (Full HD)\n")

with SentienceBrowser(
record_video_dir=str(video_dir),
record_video_size={"width": 1920, "height": 1080}, # 1080p resolution
) as browser:
print(" Resolution: 1920x1080")
browser.page.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")
browser.page.wait_for_timeout(2000)

# Close with custom filename
video_path = browser.close(output_path=video_dir / "example_1080p.webm")
print(f" ✅ Saved: {video_path}\n")

# Example 2: Custom Filename with Timestamp
print("📹 Example 2: Recording with timestamp filename\n")

timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
custom_filename = f"recording_{timestamp}.webm"

with SentienceBrowser(record_video_dir=str(video_dir)) as browser:
browser.page.goto("https://example.com")
browser.page.click("text=More information")
browser.page.wait_for_timeout(2000)

video_path = browser.close(output_path=video_dir / custom_filename)
print(f" ✅ Saved: {video_path}\n")

# Example 3: Organized by Project
print("📹 Example 3: Organized directory structure\n")

project_dir = Path("./recordings/my_project/tutorials")

with SentienceBrowser(record_video_dir=str(project_dir)) as browser:
print(f" Saving to: {project_dir}")
browser.page.goto("https://example.com")
browser.page.wait_for_timeout(2000)

video_path = browser.close(output_path=project_dir / "tutorial_01.webm")
print(f" ✅ Saved: {video_path}\n")

# Example 4: Multiple videos with descriptive names
print("📹 Example 4: Tutorial series with descriptive names\n")

tutorials = [
("intro", "https://example.com"),
("navigation", "https://example.com"),
("features", "https://example.com"),
]

for name, url in tutorials:
with SentienceBrowser(record_video_dir=str(video_dir)) as browser:
browser.page.goto(url)
browser.page.wait_for_timeout(1000)

video_path = browser.close(output_path=video_dir / f"{name}.webm")
print(f" ✅ {name}: {video_path}")

print("\n" + "=" * 60)
print("All recordings completed!")
print(f"Check {video_dir.absolute()} for all videos")
print("=" * 60 + "\n")


if __name__ == "__main__":
main()
50 changes: 50 additions & 0 deletions examples/video_recording_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Video Recording Demo - Record browser sessions with SentienceBrowser

This example demonstrates how to use the video recording feature
to capture browser automation sessions.
"""

from pathlib import Path

from sentience import SentienceBrowser


def main():
# Create output directory for videos
video_dir = Path("./recordings")
video_dir.mkdir(exist_ok=True)

print("\n" + "=" * 60)
print("Video Recording Demo")
print("=" * 60 + "\n")

# Create browser with video recording enabled
with SentienceBrowser(record_video_dir=str(video_dir)) as browser:
print("🎥 Video recording enabled")
print(f"📁 Videos will be saved to: {video_dir.absolute()}\n")

# Navigate to example.com
print("Navigating to example.com...")
browser.page.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")

# Perform some actions
print("Taking screenshot...")
browser.page.screenshot(path="example_screenshot.png")

print("Scrolling page...")
browser.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
browser.page.wait_for_timeout(1000)

print("\n✅ Recording complete!")
print("Video will be saved when browser closes...\n")

# Video is automatically saved when context manager exits
print("=" * 60)
print(f"Check {video_dir.absolute()} for the recorded video (.webm)")
print("=" * 60 + "\n")


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "sentienceapi"
version = "0.90.11"
version = "0.90.12"
description = "Python SDK for Sentience AI Agent Browser Automation"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion sentience/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
)
from .wait import wait_for

__version__ = "0.90.11"
__version__ = "0.90.12"

__all__ = [
# Core SDK
Expand Down
85 changes: 83 additions & 2 deletions sentience/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def __init__(
proxy: str | None = None,
user_data_dir: str | None = None,
storage_state: str | Path | StorageState | dict | None = None,
record_video_dir: str | Path | None = None,
record_video_size: dict[str, int] | None = None,
):
"""
Initialize Sentience browser
Expand All @@ -57,6 +59,14 @@ def __init__(
- StorageState object
- Dictionary with 'cookies' and/or 'origins' keys
If provided, browser starts with pre-injected authentication.
record_video_dir: Optional directory path to save video recordings.
If provided, browser will record video of all pages.
Videos are saved as .webm files in the specified directory.
If None, no video recording is performed.
record_video_size: Optional video resolution as dict with 'width' and 'height' keys.
Examples: {"width": 1280, "height": 800} (default)
{"width": 1920, "height": 1080} (1080p)
If None, defaults to 1280x800.
"""
self.api_key = api_key
# Only set api_url if api_key is provided, otherwise None (free tier)
Expand All @@ -80,6 +90,10 @@ def __init__(
self.user_data_dir = user_data_dir
self.storage_state = storage_state

# Video recording support
self.record_video_dir = record_video_dir
self.record_video_size = record_video_size or {"width": 1280, "height": 800}

self.playwright: Playwright | None = None
self.context: BrowserContext | None = None
self.page: Page | None = None
Expand Down Expand Up @@ -209,6 +223,17 @@ def start(self) -> None:
launch_params["ignore_https_errors"] = True
print(f"🌐 [Sentience] Using proxy: {proxy_config.server}")

# Add video recording if configured
if self.record_video_dir:
video_dir = Path(self.record_video_dir)
video_dir.mkdir(parents=True, exist_ok=True)
launch_params["record_video_dir"] = str(video_dir)
launch_params["record_video_size"] = self.record_video_size
print(f"🎥 [Sentience] Recording video to: {video_dir}")
print(
f" Resolution: {self.record_video_size['width']}x{self.record_video_size['height']}"
)

# Launch persistent context (required for extensions)
# Note: We pass headless=False to launch_persistent_context because we handle
# headless mode via the --headless=new arg above. This is a Playwright workaround.
Expand Down Expand Up @@ -390,15 +415,71 @@ def _wait_for_extension(self, timeout_sec: float = 5.0) -> bool:

return False

def close(self) -> None:
"""Close browser and cleanup"""
def close(self, output_path: str | Path | None = None) -> str | None:
"""
Close browser and cleanup

Args:
output_path: Optional path to rename the video file to.
If provided, the recorded video will be moved to this location.
Useful for giving videos meaningful names instead of random hashes.

Returns:
Path to video file if recording was enabled, None otherwise
Note: Video files are saved automatically by Playwright when context closes.
If multiple pages exist, returns the path to the first page's video.
"""
temp_video_path = None

# Get video path before closing (if recording was enabled)
# Note: Playwright saves videos when pages/context close, but we can get the
# expected path before closing. The actual file will be available after close.
if self.record_video_dir:
try:
# Try to get video path from the first page
if self.page and self.page.video:
temp_video_path = self.page.video.path()
# If that fails, check all pages in the context
elif self.context:
for page in self.context.pages:
if page.video:
temp_video_path = page.video.path()
break
except Exception:
# Video path might not be available until after close
# In that case, we'll return None and user can check the directory
pass

# Close context (this triggers video file finalization)
if self.context:
self.context.close()

# Close playwright
if self.playwright:
self.playwright.stop()

# Clean up extension directory
if self._extension_path and os.path.exists(self._extension_path):
shutil.rmtree(self._extension_path)

# Rename/move video if output_path is specified
final_path = temp_video_path
if temp_video_path and output_path and os.path.exists(temp_video_path):
try:
output_path = str(output_path)
# Ensure parent directory exists
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
shutil.move(temp_video_path, output_path)
final_path = output_path
except Exception as e:
import warnings

warnings.warn(f"Failed to rename video file: {e}")
# Return original path if rename fails
final_path = temp_video_path

return final_path

def __enter__(self):
"""Context manager entry"""
self.start()
Expand Down
Loading