Drop a video, give it a prompt, and have an LLM analyze it. Results land in your Notion page so Claude Code can pick up where you left off. Uses Apify MCP Connectors to write to Notion on your behalf — no Notion API key required.
2.8.25 — Wrong-language transcripts, cut-off answers, frame cap (2026-10-05)
New "Spoken language" field. Whisper guesses the language from the first
30 seconds. On a 26-minute English demo it heard one 9-minute part as Czech
and returned a transcript that repeated one phrase, on a run marked
successful. Set Spoken language (transcriptLanguage, e.g. en; en-US
also works) to skip the guess. The output shows the language Whisper used in
transcriptLanguageDetected (a name, e.g. english).
A looping transcript is flagged in softErrors.whisper when the same line
comes back 5+ times in a row. Checked on the real failing transcript (91%
unique overall, one line 7 times in a row) and on two clean ones.
A cut-off answer is flagged in softErrors.outputTruncated when the model
stops at the 8,000-token limit, including on cached replays. A 26-minute
breakdown stopped at minute 11 with no notice.
The frame cap is in the output. Asking for more than 12 frames still runs
with 12, and softErrors.framesCapped says so. Before, only the run log did.
Warnings lead the status message ("Done, but the transcript is missing or
unreliable…") so they show in the Console run list instead of being cut off.
Setting the language changes the cache key, so a re-run to fix a bad
transcript doesn't replay it. Runs without it keep their existing keys
(pinned in tests).
2.8.24 — Real video timestamps, whole-video coverage, fast frames (2026-10-02)
Times in the analysis are positions in the video. The Whisper transcript now
carries [m:ss] on every line and each frame is labeled with its time. Before,
the model had no times at all and fell back to clocks visible on screen.
Long videos are covered start to end. The transcript limit rose from 20,000
to 80,000 characters and the answer limit from 2,000 to 8,000 tokens. On 30-minute
videos both limits cut the breakdown off partway. The worst-case cost estimate
rises with the answer limit (8,000 output tokens on Opus 5.5 is $0.16).
Frames take seconds, not 40 minutes. Each frame is read by jumping to its
time instead of decoding the whole video, and audio is pulled in its own pass.
12 frames of a 10-minute 1080p file: 1.1 s locally.
Long answers no longer time out on the hosted route. Each model call may take
up to 5 minutes (was 45 s), since an 8,000-token answer takes about 100 s.
Re-runs don't replay older, shorter analyses. Cache keys now carry an analysis
format version, so results from before this release are not reused.
Silent videos get a frames-only analysis instead of failing at extraction.
The cost check scales with video length, so short videos with a tight cap
aren't refused for an 80,000-character transcript they don't have.
2.8.23 — Opus 4 replaced by Opus 5.5 (2026-10-02)
"Claude Opus 4" is now "Claude Opus 5.5". OpenRouter stopped serving Opus 4
("No endpoints found"), so every Opus run failed after the video work. Saved
Tasks that still say Opus 4 run on Opus 5.5 and log a warning. Opus 5.5 costs
$4 / $20 per million tokens, against $15 / $75 for Opus 4.
The model is checked before any video work. On the hosted route, the Actor
reads OpenRouter's public model list first. A model it no longer serves now
fails in seconds, not after 41 minutes of frames and transcription.
2.8.22 — Fail fast on Anthropic key problems (2026-10-02)
A pasted Anthropic key is checked before any video work. A 1-token test
request (about $0.0002, billed to your Anthropic key) catches a rejected key,
an unavailable model, or an empty credit balance in seconds. Before, a
30-minute video spent 38 minutes on frames and transcription first.
Anthropic's own reason is shown when it rejects a request, instead of only
BadRequestError.
"Just give me the analysis" ignores the Destination field. A connection
left in Destination failed the run with "unexpected ID format", even though
nothing gets written when the analysis is returned in the dataset.
Connections saved by name are accepted. The Console's connection picker
now stores ~connection-name instead of a 17-character ID. The ID check
rejected the ~.
2.8.20 — README sweep fixes (2026-09-14)
22 live runs across every platform the README names found the platforms working
and a dozen gaps between what the README promises and what runs did. Ten are fixed
here; a Whisper pre-flight that catches an out-of-credit OpenAI key is not.
Portrait videos no longer fail with HTTP 413 on default settings. Frames are
now shrunk on their longer side, so tall (portrait) videos no longer exceed the
LLM gateway's size limit at 12 frames. A 413 now gets its own message ("lower
Frames to extract") and is marked permanent.
maxChargeUsd is a ceiling for every model. Per-model prices and per-frame
token budgets (my_actor/pricing.py) replace one shared constant; GPT-4o-mini
bills a frame at roughly six times Sonnet and was under-estimated 2.4×.
Failed runs write an OUTPUT record, matching the row, as documented.
Deleted or private TikTok posts are reported as gone (permanent), not as a
retryable helper glitch.
Nested Notion writes report the child page URL. Notion now returns
app.notion.com links, which the Actor did not recognize as the new page, so
it reported the parent page instead.
Cache replays report succeeded_idempotent_replay on analysis-only runs, and
the log no longer claims the video fetch was skipped (it still runs).
provider is gemini, not google, matching the input enum and README.
A rejected API key, out-of-credit account, or invalid model is errorRecoverable: false.
Whisper is skipped when captions already cover the audio, and the status line
says when the audio wasn't transcribed (details stay in softErrors.whisper).
Caps under a cent display as $0.0010, not $0.00, and every cap refusal now says
how to override it: raise the cap or set Maximum cost for this run to 0.
2.8.12 — Restricted storage access no longer breaks runs (2026-09-14)
TikTok, other URL, and uploaded-file runs no longer fail with "The run hit an
unexpected error" when your Apify account has General resource access set
to Restricted (the default for new accounts). Every video the Actor
processes is first stored in your account — the file you upload, or the file a
download helper saves — and the Actor read it back without your token, so
restricted accounts got 403.
Reads from your Apify account are now signed with the run's own token.
Only requests to api.apify.com carry it; redirects to other hosts never do.
A failed file fetch now says what happened. Instead of "unexpected
error" you get the cause, whether a retry makes sense, and the "no LLM cost
was incurred" line. Agent callers get download_failed_infra (retryable,
with errorRetryAfterSec) or download_video_gone (permanent).
Concurrent-run limit named. When the hosted OpenRouter helper can't
start because your account is at its concurrent-run limit, the message says
so instead of "OpenRouter returned a server error (HTTP 500)".
2.8.10 — Long-video opt-in (2026-09-01)
New input allowLongVideos (default false) lifts the 10-minute duration cap
for users who accept the higher, harder-to-estimate cost. Off, nothing changes.
New Advanced toggle: "Process videos longer than 10 minutes (costs more)".
The schema copy is explicit that cost scales with length and is hard to
estimate up front, and points at maxChargeUsd — which still works: the
post-ingest worst-case estimate uses the real duration, so a cap aborts
before the LLM call exactly as before.
File pipeline: videos over 10 minutes no longer hard-fail when the toggle
is on; instead the run logs and shows a long-video cost warning in the status
line (including whether a cost cap is set). The refusal message when the
toggle is off now names the toggle.
YouTube visual path: a visual prompt on a >10-min video downloads real
frames when the toggle is on, instead of falling back to transcript +
thumbnail. Fallback messages now mention the toggle.
Downloader timeout: sub-actor download timeout goes 6 → 20 minutes on
long-video runs so 1-hour downloads aren't killed mid-flight.
Whisper 25 MB guard: audio over Whisper's API file limit (~69 minutes at
our 48 kbps mono encoding) skips transcription with a clear
softErrors.whisper message instead of burning a doomed API upload. The
constant is decimal 25,000,000 bytes so the "~69 minutes" copy is exact and
the guard is conservative (review catch: 25 MiB would shift the boundary to
~73 min and could let a doomed upload through).
Early budget gate (review blocker fix): the pre-LLM cost gate used to fire
after Whisper had billed — on long videos, the cost that scales. The file
pipeline now checks the cap right after reading the video's duration, before
frame extraction and transcription, assuming the transcript hits its
truncation cap. Cap-selection and publisher-fee math are shared with the
refined gate via new resolve_charge_cap() / publisher_fees_estimate_usd()
helpers, and the refined gate's abort message now names any Whisper spend
that already happened.
allowLongVideos joins the idempotency key (review blocker fix): the
fallback copy tells users to flip the flag and re-run; without the flag in
the key that exact re-run replayed the stale transcript-only result.
Existing idempotency entries miss once and recompute; the analysis cache
still absorbs the LLM cost where the resolved mode matches.
Whisper empty-result fix (pre-existing, amplified by long videos): a
silent video no longer sets whisper_used (which added a phantom per-minute
term to cost estimates) or clobbers a preserved caption transcript; it now
soft-errors "no audio track".
Limits that do not lift: 1 GB file size, 12-frame cap, 20,000-character
transcript truncation. README documents all of this under Long videos.
Deferred from review, with reasoning: (a) the downloader's one infra retry
keeps the same 20-min timeout on long runs (worst case ~40 min parent wait) —
acceptable for an explicit opt-in, revisit if real runs hit it; (b) passing a
cost cap down to the sub-actor call — needs SDK signature verification first;
(c) surfacing transcript truncation in softErrors — output-schema addition,
separate change.
2.8.9 — YouTube ingest fixes (2026-08-03)
Fixes the three bugs found in the 2026-08-03 demo-review runs (fCaYYYdXwRagdgqiE,
TuRJhQp0HXRnbsugw). No input schema changes; one new analysisMode value.
YouTube transcripts extract again.streamers/youtube-scraper returns
captions as one complete raw SRT per subtitles entry ({language, srt}), not
the {text, start} segment shape the extractor expected — so runs recorded
transcriptChars: 0 while a 50 KB SRT sat in the response. Added an SRT
flattener (strips indices, timestamps, and the rolling-caption duplicates
auto-generated tracks emit). The demo video now yields 12,050 transcript chars.
No more silent caption failures.softErrors.youtubeTranscript used to stay
null unless BOTH transcript and title were missing. It now reports the
metadata-present-but-no-captions case, with the recovery step in the message.
Thumbnail-only runs stop pretending. A YouTube run with no usable transcript
was labeled transcript+thumbnail and the LLM was told to lean on a transcript
that wasn't there — which produced confident fabricated reviews. Those runs are
now labeled analysisMode: "thumbnail" (new value, documented in the README),
and the prompt tells the model it has one still image, forbids inventing
content, and redirects it to what the thumbnail and metadata do show.
Poisoned-cache guard. Pre-fix cached analyses were computed without the
transcript this build now extracts; replaying them would resurface the
fabricated answers. A cache hit is refused when the cached run had no
transcript but the current run does, and thumbnail runs refuse mode-less
pre-2.7 cache entries.
Downloader retry.download_via_actor retries once after a 10 s pause on
infra failures (launch timeouts), with a log line for the wait. Deterministic
failures are not retried. This does not fix a persistently-down downloader —
a second fallback is a known follow-up, not in this build.
13 new tests (SRT edge cases pinned to the verbatim scraper payload, retry
control flow, and an analysisMode contract test that fails if the code and
README enum drift).
2.8.8 — crash reliability (2026-06-24)
Build reproducibility and memory rightsizing. No input/output schema changes.
Reproducible builds (lockfile). Added requirements.lock, a fully pinned,
hash-verified resolution compiled from requirements.txt. The Docker build now
installs from it with pip install --require-hashes -r requirements.lock instead
of from the loose ranges. This pins apify-shared (currently 2.2.0) — the
transitive dependency behind the v2.x crash — so a rebuild can't silently pull a
changed version. requirements.txt keeps the human-readable ranges and is the
source the lock is compiled from. Regenerate the lock after editing
requirements.txt with:
uv pip compile requirements.txt -o requirements.lock --generate-hashes --no-header --python-version 3.14
(--python-version 3.14 matches the apify/actor-python:3.14 base image;
pre-releases are excluded by default). The lock in this commit was generated this
way on a connected machine, not hand-written.
Memory rightsizing. Lowered .actor/actor.json from min 2048 / max 4096 MB to
min 1024 / max 2048 MB. Peak observed Python RSS was ~114 MB; the 2 GB max keeps
headroom for the ffmpeg subprocess (whose RSS is not in that 114 MB number) while
halving the per-GB actor-start charge.
Follow-up (not yet done): run an on-platform headroom test on a long /
high-resolution video to confirm 2 GB is enough for ffmpeg before considering a
further drop to 1024 MB max. Watch the run for OOM kills / ffmpeg exit 137. Do
not lower the max below 2048 MB until that test passes.
v2.8 — 2026-06-12
Error-handling hardening. No input/output schema changes; existing Tasks keep working.
Added a top-level exception net in main(). An unhandled error now fails the
run with a failed_internal dataset row (errorCategory: internal_error,
errorRecoverable: false) and a human status message, instead of exiting with
a bare exit_code 1 and no explanation.
Split the LLM-call handler. Known RuntimeErrors from the LLM paths
(transport, 4xx/5xx, out-of-credit, rate limit) stay user-friendly and
recoverable. Any other exception is treated as an internal error (permanent),
so a bug isn't mislabeled as a retryable LLM condition and looped on by agents.
Both failure messages state "No LLM analysis charge was incurred" where that's
provably true, and keep raw exception types out of the lead of the message.
First release built from the GitHub repo (private) rather than uploaded source files.
Earlier
For 2.7 and earlier, see the Apify Console build history and the project notes.