Skip to content
Open
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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Get your API token from [Apify Console](https://console.apify.com/settings/integ

## Tools

The package ships dedicated tools across three families plus a generic "wrap any Actor by ID" tool for everything else. All return a uniform `{"run": {...}, "items": [...]}` JSON envelope (parse with `json.loads`).
The package ships dedicated tools across four families plus a generic "wrap any Actor by ID" tool for everything else. All return a uniform `{"run": {...}, "items": [...]}` JSON envelope (parse with `json.loads`).

### Core tools

Expand Down Expand Up @@ -122,6 +122,28 @@ result = ApifyInstagramScraperTool().invoke({
print(json.loads(result))
```

### Transcription tools

Spoken words out of video and audio, rather than what a page shows. Available as `APIFY_TRANSCRIPT_TOOLS`:

- `ApifyFacebookAdsTranscriptTool`: transcripts, hooks and CTAs of the Facebook Ad Library ads running now
- `ApifyYouTubeTranscriptTool`: transcripts of specific YouTube videos
- `ApifyMediaTranscriberTool`: transcripts of audio/video file links and supported podcast or video pages

These three wrap pay-per-event Actors published by `steadyfetch`; a run is charged to your Apify account at the price on each Actor's store page.

```python
import os, json
from langchain_apify import ApifyYouTubeTranscriptTool

os.environ["APIFY_TOKEN"] = "YOUR_APIFY_TOKEN"

result = ApifyYouTubeTranscriptTool().invoke({
"video_urls": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"],
})
print(json.loads(result))
```

### Using tools with an agent

Each convenience list lets you bind a whole tool family to an agent in one line. Don't bind all tools at once. Most LLMs lose routing accuracy past ~8 tools, so pick the family the agent actually needs.
Expand Down
9 changes: 9 additions & 0 deletions langchain_apify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
APIFY_CORE_TOOLS,
APIFY_SEARCH_TOOLS,
APIFY_SOCIAL_TOOLS,
APIFY_TRANSCRIPT_TOOLS,
ApifyActorsTool,
ApifyEcommerceScraperTool,
ApifyFacebookAdsTranscriptTool,
ApifyFacebookPostsScraperTool,
ApifyGetDatasetItemsTool,
ApifyGoogleMapsTool,
Expand All @@ -19,6 +21,7 @@
ApifyLinkedInProfileDetailTool,
ApifyLinkedInProfilePostsTool,
ApifyLinkedInProfileSearchTool,
ApifyMediaTranscriberTool,
ApifyRAGWebBrowserTool,
ApifyRunActorAndGetDatasetTool,
ApifyRunActorTool,
Expand All @@ -29,6 +32,7 @@
ApifyTwitterScraperTool,
ApifyWebCrawlerTool,
ApifyYouTubeScraperTool,
ApifyYouTubeTranscriptTool,
)
from langchain_apify.wrappers import ApifyWrapper

Expand Down Expand Up @@ -70,10 +74,15 @@
'ApifyGoogleMapsTool',
'ApifyYouTubeScraperTool',
'ApifyEcommerceScraperTool',
# Transcription Actor tools
'ApifyFacebookAdsTranscriptTool',
'ApifyYouTubeTranscriptTool',
'ApifyMediaTranscriberTool',
# Tool group lists
'APIFY_CORE_TOOLS',
'APIFY_SOCIAL_TOOLS',
'APIFY_SEARCH_TOOLS',
'APIFY_TRANSCRIPT_TOOLS',
# Meta
'__version__',
]
117 changes: 117 additions & 0 deletions langchain_apify/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
_DEFAULT_SCRAPE_TIMEOUT_SECS,
_DEFAULT_SOCIAL_RESULTS_LIMIT,
_DEFAULT_SOCIAL_TIMEOUT_SECS,
_DEFAULT_TRANSCRIPT_RESULTS_LIMIT,
_DEFAULT_TRANSCRIPT_TIMEOUT_SECS,
_DEFAULT_YOUTUBE_MAX_RESULTS,
)
from langchain_apify._error_messages import (
_ERROR_ACTOR_RUN_FAILED,
_ERROR_APIFY_TOKEN_ENV_VAR_NOT_SET,
_ERROR_EMPTY_INPUT_LIST,
_ERROR_SCRAPE_EMPTY,
)
from langchain_apify._types import CrawlerType # noqa: TCH001 # runtime-needed: pydantic-free annotation
Expand Down Expand Up @@ -57,6 +60,11 @@
_TIKTOK_ACTOR_ID = 'clockworks/tiktok-scraper'
_FACEBOOK_ACTOR_ID = 'apify/facebook-posts-scraper'

# Actor IDs - transcripts.
_FACEBOOK_ADS_TRANSCRIPT_ACTOR_ID = 'steadyfetch/facebook-ads-transcript-scraper'
_YOUTUBE_TRANSCRIPT_ACTOR_ID = 'steadyfetch/youtube-transcript-scraper'
_MEDIA_TRANSCRIBER_ACTOR_ID = 'steadyfetch/media-transcriber'

# Accepted parameter values validated client-side before a run.
_YOUTUBE_SEARCH_TYPES = ('search', 'video', 'channel')
_ECOMMERCE_URL_TYPES = ('product', 'category')
Expand Down Expand Up @@ -850,6 +858,115 @@ def crawl_website(
dataset_items_limit=max_crawl_pages,
)

def facebook_ads_transcript_scrape(
self,
search_queries: list[str],
country: str = 'US',
max_results: int = _DEFAULT_TRANSCRIPT_RESULTS_LIMIT,
timeout_secs: int = _DEFAULT_TRANSCRIPT_TIMEOUT_SECS,
) -> tuple[dict, list[dict]]:
"""Transcribe Facebook Ad Library ads via ``steadyfetch/facebook-ads-transcript-scraper``.

Args:
search_queries: Keywords or advertiser page names to search the Ad Library for.
country: Two-letter country code the ads are served in.
max_results: Maximum number of ad creatives to return.
timeout_secs: Maximum time to wait for the run to finish.

Returns:
A ``(run_details, items)`` tuple.

Raises:
ValueError: If ``search_queries`` is empty.
RuntimeError: If the Actor run does not succeed.
"""
self._require_non_empty(search_queries, 'search_queries')
run_input: dict = {
'searchQueries': list(search_queries),
'country': country,
'searchMaxAds': max_results,
'maxAds': max_results,
}
return self.run_actor_and_get_items(
_FACEBOOK_ADS_TRANSCRIPT_ACTOR_ID,
run_input=run_input,
timeout_secs=timeout_secs,
dataset_items_limit=max_results,
)

def youtube_transcript_scrape(
self,
video_urls: list[str],
max_results: int = _DEFAULT_TRANSCRIPT_RESULTS_LIMIT,
timeout_secs: int = _DEFAULT_TRANSCRIPT_TIMEOUT_SECS,
) -> tuple[dict, list[dict]]:
"""Transcribe YouTube videos via ``steadyfetch/youtube-transcript-scraper``.

Args:
video_urls: Watch, youtu.be, ``/shorts/`` or ``/live/`` links, or bare video IDs.
max_results: Maximum number of transcripts to return.
timeout_secs: Maximum time to wait for the run to finish.

Returns:
A ``(run_details, items)`` tuple.

Raises:
ValueError: If ``video_urls`` is empty.
RuntimeError: If the Actor run does not succeed.
"""
self._require_non_empty(video_urls, 'video_urls')
run_input: dict = {'videoUrls': list(video_urls), 'maxItems': max_results}
return self.run_actor_and_get_items(
_YOUTUBE_TRANSCRIPT_ACTOR_ID,
run_input=run_input,
timeout_secs=timeout_secs,
dataset_items_limit=max_results,
)

def media_transcribe(
self,
urls: list[str],
max_results: int = _DEFAULT_TRANSCRIPT_RESULTS_LIMIT,
timeout_secs: int = _DEFAULT_TRANSCRIPT_TIMEOUT_SECS,
) -> tuple[dict, list[dict]]:
"""Transcribe audio or video files via ``steadyfetch/media-transcriber``.

Args:
urls: Direct audio/video file links, or page links on a supported host.
max_results: Maximum number of transcripts to return.
timeout_secs: Maximum time to wait for the run to finish.

Returns:
A ``(run_details, items)`` tuple.

Raises:
ValueError: If ``urls`` is empty.
RuntimeError: If the Actor run does not succeed.
"""
self._require_non_empty(urls, 'urls')
run_input: dict = {'urls': list(urls)}
return self.run_actor_and_get_items(
_MEDIA_TRANSCRIBER_ACTOR_ID,
run_input=run_input,
timeout_secs=timeout_secs,
dataset_items_limit=max_results,
)

@staticmethod
def _require_non_empty(values: list[str], field: str) -> None:
"""Raise if a required list input is empty.

Args:
values: The list supplied by the caller.
field: Name of the field, used in the error message.

Raises:
ValueError: If ``values`` is empty.
"""
if not values:
msg = _ERROR_EMPTY_INPUT_LIST.format(field=field)
raise ValueError(msg)

@staticmethod
def _check_run_status(run: dict) -> None:
"""Raise if the run did not succeed."""
Expand Down
4 changes: 4 additions & 0 deletions langchain_apify/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
_DEFAULT_RUN_TIMEOUT_SECS = 300
_DEFAULT_SCRAPE_TIMEOUT_SECS = 120
_DEFAULT_SOCIAL_TIMEOUT_SECS = 600
_DEFAULT_TRANSCRIPT_TIMEOUT_SECS = 600

# Default result / page limits, by operation.
_DEFAULT_DATASET_ITEMS_LIMIT = 100
Expand All @@ -27,6 +28,9 @@
_DEFAULT_GOOGLE_MAPS_MAX_RESULTS = 10
_DEFAULT_YOUTUBE_MAX_RESULTS = 10
_DEFAULT_ECOMMERCE_MAX_RESULTS = 20
# Transcription Actors charge per delivered item, so the family defaults to a
# smaller batch than the scraping families.
_DEFAULT_TRANSCRIPT_RESULTS_LIMIT = 10

# Upper-bound clamp ceilings applied by _ApifyGenericTool to LLM-supplied
# values. The Pydantic Field defaults on the base class reference these so
Expand Down
2 changes: 2 additions & 0 deletions langchain_apify/_error_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

_ERROR_SCRAPE_EMPTY = 'No content extracted from {url}.'

_ERROR_EMPTY_INPUT_LIST = '{field} must contain at least one value.'

_NOTICE_TWITTER_DEMO = (
'The Twitter/X Actor returned demo placeholder data instead of real tweets.'
' This happens on the Apify free plan; a paid Apify plan is required to scrape real tweets.'
Expand Down
16 changes: 16 additions & 0 deletions langchain_apify/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,26 @@
TwitterSearchMode,
TwitterSort,
)
from langchain_apify.tools.transcripts import (
APIFY_TRANSCRIPT_TOOLS,
ApifyFacebookAdsTranscriptInput,
ApifyFacebookAdsTranscriptTool,
ApifyMediaTranscriberInput,
ApifyMediaTranscriberTool,
ApifyYouTubeTranscriptInput,
ApifyYouTubeTranscriptTool,
)

__all__ = [
'APIFY_CORE_TOOLS',
'APIFY_SEARCH_TOOLS',
'APIFY_SOCIAL_TOOLS',
'APIFY_TRANSCRIPT_TOOLS',
'ApifyActorsTool',
'ApifyEcommerceScraperInput',
'ApifyEcommerceScraperTool',
'ApifyFacebookAdsTranscriptInput',
'ApifyFacebookAdsTranscriptTool',
'ApifyFacebookPostsScraperInput',
'ApifyFacebookPostsScraperTool',
'ApifyGetDatasetItemsInput',
Expand All @@ -82,6 +94,8 @@
'ApifyLinkedInProfilePostsTool',
'ApifyLinkedInProfileSearchInput',
'ApifyLinkedInProfileSearchTool',
'ApifyMediaTranscriberInput',
'ApifyMediaTranscriberTool',
'ApifyRAGWebBrowserInput',
'ApifyRAGWebBrowserTool',
'ApifyRunActorAndGetDatasetInput',
Expand All @@ -102,6 +116,8 @@
'ApifyWebCrawlerTool',
'ApifyYouTubeScraperInput',
'ApifyYouTubeScraperTool',
'ApifyYouTubeTranscriptInput',
'ApifyYouTubeTranscriptTool',
'InstagramSearchType',
'TikTokSearchType',
'TwitterSearchMode',
Expand Down
Loading
Loading