---
name: earnings-commentary-drift-memo
description: "Compare what a CEO said about one topic across up to eight quarterly results videos with Vivu, next to the figure shown on screen, and write a drift table and memo. Use when covering a company."
---Earnings commentary drift memo with Vivu
This skill turns a company name, a topic and a list of quarters into a side by side record of how management talked about that topic, quarter by quarter. Claude finds the company's official quarterly results videos, downloads them with their captions, prices the job against the user's Vivu plan, indexes the videos in Vivu, runs two standing searches (the moment the CEO talks about the topic, and the key figure card shown on screen), checks every moment against the captions and against frames from the video, and writes drift_table.csv and a one page drift_memo.md with the exact words and a timestamped link for each quarter.
The value is in two things a transcript does not hold. The key figure card on screen carries the number the company chose to show, to one decimal ("Adjusted operating margin 12.0%"), while the spoken line rounds it ("a margin of 12%") and auto captions garble the currency. And each quarter's answer sits somewhere different in the video, so lining up eight quarters by hand means rewatching all of them. Vivu finds the moments; the exact words come from captions or the company's own transcript, never from Vivu's reason text, which paraphrases. Every row in the table is checked against the source before it counts.
When to use
Use when someone asks to "compare what the CEO said about tariffs over the last eight quarters", "track how management's language on North America changed", "line up the quarterly CEO comments on margins", or wants a drift memo before an earnings call. It fits companies that publish a results video every quarter: a short CEO comment video, or a results webcast with slides.
Not for:
- A company whose calls are audio only with a static slide. There is nothing on screen to read, so use the official transcripts directly and skip Vivu.
- A one off question about a single quarter's video. Search Vivu directly.
- Investment advice. The memo records what was said and shown; it does not recommend buying or selling.
Working principles
- Report measured numbers, not estimates. When a number is an estimate, say so.
- Nothing is "verified" until it has been checked against the source video. Search results stay candidates until triaged; quarters without a checked moment are shown as "not found", never filled in.
- Stop and tell the user when a required capability or tool is missing. Do not guess around it.
- Ask the user before anything that is expensive to redo or that acts on their behalf: the download list, indexing (it uses plan minutes), the table layout, and any write to Notion or another shared tool.
- Quotes come from the words actually spoken, taken from captions or the company's transcript. Vivu's reason text is a paraphrase, not a transcript, so it never goes into a quote.
- The Vivu result page link expires after four hours, so it only appears in the live reply. The table and memo link to the user's own source (the YouTube or IR page link with a time parameter).
What you need before starting
The skill runs in Claude Code on the user's computer (a terminal or the Code tab of Claude Desktop), because it needs a shell, local files and, for YouTube, a residential IP. It runs once per request; nothing is scheduled. Check each item at the start of the run and tell the user plainly what is missing before doing anything else.
| Requirement | Why | How to check |
|---|---|---|
| Vivu connector with write access | create a project, open its upload page, search | vivu_get_account shows can_create_projects: true (tool names may carry a server prefix). "has not granted vivu.write" means the connection is read only: reconnect Vivu and allow write access. No Vivu tools at all: add the Vivu connector (https://mcp.vivu.ai/mcp) and stop |
| A shell with ffmpeg and ffprobe | measure minutes, extract frames to read the figure cards | ffmpeg -version |
| yt-dlp and node on a machine with a residential IP | download the videos and captions; YouTube blocks cloud and datacenter IPs | yt-dlp --version, node --version, then the one video test download in Step 3 |
| English captions or the company's transcript for each quarter | the exact words; Vivu does not return a transcript | the caption file exists after Step 3, or the user points to the transcript PDFs |
| Web fetch or search | find the company's results videos in Step 2 | fetch the company's investor relations page once |
| Only if the user wants a Notion page: the Notion connector | Step 8 | the Notion tools are listed and a search call works |
Inputs to collect
Ask for anything missing, most important first.
- The company (required).
- The topic, in the user's words (required). Example: "North America, US tariffs and trade policy".
- The quarters. Default: the last eight published quarters.
- The on screen figure to read next to each quarter. Default: the adjusted operating margin card, if the videos show key figure cards.
- Which video per quarter: the short CEO comment video or the full results webcast. Default: the short video when it exists, because eight webcasts of about an hour each do not fit a Premium plan (Step 4).
- Where the output goes. Default: drift_table.csv and drift_memo.md in the working folder; a Notion page only if the user asks.
Files and state
Keep everything in one working folder on the computer that holds the videos:
earnings-drift/
config.json company, topic, quarters, queries, figure to read, project_id
quarters.csv quarter, source_url, file, duration_s, caption_file, video_id
archive.txt yt-dlp download archive
videos/ one .mp4 per quarter, nothing else
captions/ one .vtt per quarter
results/ the raw result of each search, one JSON file per query
frames/ frames extracted to check hits and read the figure cards
drift_table.csv one row per quarter (Step 7)
drift_memo.md the one page memo (Step 7)
state.json downloaded IDs, uploaded video_ids, searches run with job_id, quarters finished
A rerun reads state.json first and skips what is done: videos already downloaded or in the project, searches already run, quarters already in the table. When the user adds next quarter's video later, only that video is downloaded and indexed; the two searches are run again because a search covers the whole project, and rows already in drift_table.csv are kept.
Step 1: Check the Vivu connector and the setup
Goal: every row of What you need is present, or the user knows exactly what is missing.
- Call vivu_get_account. If the Vivu tools are missing, tell the user to add the Vivu connector in Claude (https://mcp.vivu.ai/mcp) and stop. The account must show can_create_projects: true. If a later write call fails with "has not granted vivu.write", ask the user to reconnect Vivu and allow write access, then retry.
- Run ffmpeg -version, yt-dlp --version and node --version. Without ffmpeg nothing can be checked; stop and say so.
- If the user wants a Notion page, check that the Notion tools are listed.
Done when vivu_get_account shows can_create_projects: true and the user has been told which optional tools are missing.
Step 2: Find the videos for each quarter
Goal: one video per quarter, from the company itself, with its link and publish date.
- Fetch the company's investor relations page and look for a results video or webcast per quarter, and for a YouTube channel link.
- On YouTube, list the channel's uploads and keep titles that name a quarter (for example "Q2/2025", "second quarter", "interim report"):
yt-dlp --flat-playlist --playlist-end 300 -O "%(id)s|%(duration)s|%(title)s" "https://www.youtube.com/channel/CHANNEL_ID/videos"
CHANNEL_ID is the UC... ID from the channel page. If a handle URL returns "HTTP Error 404: Not Found", read the channel ID from one of the company's videos (yt-dlp --skip-download -O "%(channel_id)s" VIDEO_URL, where VIDEO_URL is any of its video links) and list by ID. 3. Check that each video comes from the company's own channel or IR site. Reposts by third party "earnings" channels are common; do not use them. 4. Show the user the table of quarter, title, duration and link. Mark quarters with no video as missing.
Done when the user approves the list of quarters and videos, and quarters.csv holds one row per quarter.
Step 3: Download the videos and captions
Goal: every approved video and its captions in the working folder.
Run on the residential IP machine. Test one video before the batch. 720p keeps the figure cards readable at a fraction of the 1080p size:
yt-dlp --js-runtimes node --restrict-filenames \
-f "bv*[height<=720]+ba/b[height<=720]" --merge-output-format mp4 \
--match-filter "duration<=MAX_SECONDS" \
--download-archive earnings-drift/archive.txt \
-P "temp:SCRATCH_DIR" -P "home:earnings-drift/videos" \
-o "%(id)s_%(title).60B.%(ext)s" --retries 5 \
"https://www.youtube.com/watch?v=VIDEO_ID"
VIDEO_ID is the 11 character ID, MAX_SECONDS the longest video in seconds, SCRATCH_DIR any folder outside videos/. If yt-dlp stops with "HTTP Error 403: Forbidden", retry once with --extractor-args "youtube:player_client=web_embedded" added, and check the resolution with ffprobe afterwards. The youtube-competitor-watch skill (https://vivu.ai/skills/youtube-competitor-watch) covers channel verification and download troubleshooting in more depth.
Then the captions, English only, with a pause between requests:
yt-dlp --skip-download --write-subs --write-auto-subs --sub-langs "en,en-US" --sub-format vtt \
--sleep-requests 3 --sleep-subtitles 5 \
-P "earnings-drift/captions" -o "%(id)s" "https://www.youtube.com/watch?v=VIDEO_ID"
Asking for many languages at once triggers "HTTP Error 429: Too Many Requests". If a video has no English track under "en", list what exists with --list-subs and ask for that name (uploader captions are often "en-US"). Uploader captions are better than auto captions; auto captions mishear names and currencies. When the company publishes an official transcript, use it for quotes and keep the captions for timing.
Measure each file: ffprobe -v error -show_entries format=duration -of csv=p=0 FILE. Write duration_s and caption_file into quarters.csv.
Done when videos/ holds one verified .mp4 per approved quarter and captions/ holds a caption file for each, or the user has been told which quarter has none.
Step 4: Size the job and get approval
Goal: the user sees the index minutes and search credits before anything is uploaded.
- Sum the durations in quarters.csv to minutes.
- Searches: two precise searches per run (topic and figure card), plus one rewording each if the first page is not clean. A precise search uses 5 credits in total; a fast search uses 1 credit. Label the total as an estimate.
- Call vivu_get_usage for the plan and what remains. The plans: Free is $0 a month with 20 indexing minutes a month and 50 search credits a month; Premium is $30 a month with 180 indexing minutes a month and 500 search credits a month.
- Show one table:
| This job | Remaining this month | Fits | |
|---|---|---|---|
| Index minutes | measured sum | from vivu_get_usage | yes or no |
| Search credits (estimate) | 4 x 5 | from vivu_get_usage | yes or no |
Eight full webcasts of about an hour each do not fit a Premium plan. If the job does not fit, offer these levers in order: use the short CEO comment videos where the company publishes them; for full webcasts, index only the presentation part with the slides (cut it locally with ffmpeg -ss 0 -to SECONDS -i FILE -c copy OUT.mp4, where SECONDS is the time the transcript marks the start of Q&A, FILE the webcast and OUT a new name in videos/) and compare the Q&A from the transcript; cover fewer quarters; spread the job over two months; move to a larger plan. Say which quarters or parts are left out. Cutting webcasts was not exercised in our test run.
If vivu_get_usage returns no remaining figures (some admin or team accounts return null), show the needed minutes and credits anyway and ask the user to confirm the allowance.
Done when the user approves the video list and the cost table.
Step 5: Upload and index
Goal: every approved video is ready in one Vivu project.
- Call vivu_list_projects. Reuse a project for this company if one exists. Otherwise call vivu_create_project with a name like "Earnings drift COMPANY" and visibility "private"; the default is "organization", which every member of the Vivu workspace can see. Record the project_id in state.json.
- Call vivu_open_upload_page with the project_id right before the upload. The link expires in 180 seconds, so request it only when the user or the browser tool is ready, and never paste it into a message or file.
- Give the link to the user to open in their own browser and select the files in videos/, or open it with a browser tool that can attach local files. Claude in Chrome accepts at most 10 MB per upload call; files above that go through the user's browser or the Vivu web app. Never split or recompress a video to fit.
- Vivu replaces spaces and brackets in file names with underscores. Match videos back to quarters.csv by the 11 character YouTube ID at the start of the file name.
- Poll vivu_list_videos every 30 seconds or so until every video shows status ready. Write each video_id into quarters.csv and state.json.
The upload through the user's own browser was not exercised in our test run; the test videos reached Vivu through a different upload path. In our test run, eight videos (25.2 minutes) were all ready about 8 minutes after the upload link was requested. Poll instead of guessing; longer webcasts will take longer.
Done when vivu_list_videos shows every approved quarter as ready and quarters.csv has a video_id on each row.
Step 6: Search the topic and the figure card, and check every hit
Goal: for each quarter, the checked moments where the topic is discussed and the checked figure card, with every false positive counted.
The standing queries, one row per output field. Replace the topic with the user's topic, in the words an analyst would use:
| Field | Query | Mode | maximum_results |
|---|---|---|---|
| said (topic moment) | the CEO talks about the North American truck market, US tariffs or trade policy and what it means for demand, orders or production | precise | 20 |
| shown (figure card) | a key figures card on screen showing the adjusted operating margin percentage | precise | 20 |
The chain: the first search finds, in every quarter, the window where the topic is discussed, and the captions inside that window give the exact words; the second flips from what was said to what was shown and finds the card with the company's own number, which a local frame then reads. Both searches are precise because every row needs a timestamp, and only precise returns one; fast returns the whole file with an empty reason. maximum_results is 20 because eight quarters with two or three mentions each fit under it, and it is also the ceiling on what can be found: raise it for more quarters or longer videos.
- Run each query with vivu_search_videos (project_id, query, mode "precise", maximum_results 20). It returns a job_id. Call vivu_get_search_results until complete is true; each status call can wait up to 45 seconds, so a pending search is not a stalled one. Save the completed result as results/FIELD.json. The result page link may go in the live reply; it expires after four hours, so it never goes into a file.
- Check every topic hit against the words. Open the quarter's caption file and read the cues whose timestamps fall inside the hit's window. The hit is real when the CEO says something about the topic there; it is a false positive when the window only has neighboring talk. Read vivu_get_video_summary with include_segments true and start_ms and end_ms around the hit as a second signal only: its sections are chapter level and their titles can miss the topic.
- Copy the quote from the captions (or the official transcript), never from the reason text. The reason paraphrases and can widen or narrow what was said. Mark auto caption quotes NOT VERIFIED until someone has listened to the moment, because auto captions mishear names, currencies and numbers.
- Check every card hit with frames. Extract three frames inside the window (start, middle, half a second before the end):
ffmpeg -v error -ss SECONDS -i FILE -frames:v 1 -q:v 3 earnings-drift/frames/FIELD_hN_POSITION.png
SECONDS is the time in seconds (start_ms divided by 1000, and so on), FILE the local video, FIELD_hN_POSITION a name such as margin_card_h2_mid. The hit is real when a frame shows the card with the figure's label. It is a false positive when the frames show only another figure (net sales, operating income) or no card. 5. Read the number only from a settled frame. Key figure cards often count up from a low number, so a frame taken early shows a wrong value. Extract every half second across the card and read the last frame before it fades:
ffmpeg -v error -ss START -to END -i FILE -vf "fps=2,scale=427:-1,tile=5x6" -frames:v 1 earnings-drift/frames/FIELD_hN_every_half_second.png
START and END are the window in seconds. The tile holds 30 frames, 15 seconds. For a longer window, run it again with -ss set to START plus 15 and a new file name, so the last frame before the card fades is always in a strip. Record the settled value, the frame and its time. When the reason text and the settled frame disagree, the frame wins. 6. After both searches, search each caption file for the topic words (for example grep -i -e "north america" -e "tariff" -e "trade" CAPTION_FILE, where CAPTION_FILE is the quarter's file in captions/). Vivu finds the main passage in each quarter but can skip a short passing mention; add any line it missed to the table with "caption check only" as its source, and never count it as a Vivu hit. 7. Show the user, per quarter: topic hits returned, real, false, the settled figure, quarters with no topic hit, and lines found only by the caption check. A quarter with no hit and nothing in the captions is "not mentioned", which is itself a finding.
Vivu returns a time range that contains the moment, not an exact frame, and the range is usually wider than the moment: topic windows can start on the greeting or the line before. Take topic_start_s from the first caption cue of the quote, not from start_ms.
Speaker identification was not tested. Results videos with one speaker need nothing more; for a webcast with several speakers, take the speaker from the name bar on screen or the transcript's speaker labels and mark the source.
Worked example from our test run
The corpus was eight public quarterly CEO comment videos from one European truck maker's official channel, one per quarter over the last two years (25.2 minutes). Truth came from the captions and from contact sheets of every video, written before any search ran except one full year card, added after the card search found it. Neither search was reworded.
The topic search returned 8 moments, 8 real and 0 false. Every quarter that discussed the topic got a real hit, and the two quarters that never mention it (both year end videos) got none. It missed 5 of the 13 truth segments, a recall of 62%; all 5 were short passing mentions, four of them in an opening or closing line, and a grep of the caption files found every one. The median window was 23 seconds.
The card search returned 10 moments, 10 real and 0 false, with 0 missed, including a full year card our own contact sheet had skipped. The median window was 8.5 seconds. Frames taken while the cards were counting up read wrong values; the settled frames matched what the reason text said.
The reason text never matched the captions word for word, and once turned "in Europe and North America, volumes have come down" into a line about North America alone. The two searches were precise, an estimate of 10 credits for the test run.
Done when every returned hit is marked verified or false_positive in state.json, each quarter has a figure value read from a settled frame or "not found", and the counts have been shown to the user.
Step 7: Build the drift table and memo, and approve a sample
Goal: drift_table.csv with one row per quarter and a one page memo, approved by the user.
- Write drift_table.csv, one row per quarter, oldest first. Sample row (placeholder values):
quarter,video_file,source_url,topic_start_s,topic_mmss,said_verbatim,said_source,margin_card_value,card_frame,card_mmss,status
Q2/2025,VIDEO_ID_Q2_2025_CEO_comment.mp4,https://www.youtube.com/watch?v=VIDEO_ID&t=80,80,01:20,"EXACT WORDS FROM CAPTIONS",auto captions (NOT VERIFIED by ear),11.0%,frames/margin_card_h1_every_half_second.png,00:39,verified
| Field | Source | If unavailable |
|---|---|---|
| topic_start_s, topic_mmss | the first caption cue of the quote inside a verified hit's window | blank, with said_verbatim "not mentioned" |
| said_verbatim | captions or the official transcript, word for word | "not mentioned" when neither the search nor the caption check found a line |
| said_source | which text the quote came from | always filled |
| margin_card_value, card_frame, card_mmss | the settled frame from Step 6 | "not found"; never the reason text or the spoken number |
| source_url | the company's own link plus &t=SECONDS (or the host's time parameter) | the file name plus MM:SS |
| status | verified, caption check only, or candidate | candidate |
Say which fields are inferred: none of the quotes are, but auto caption quotes are NOT VERIFIED until heard, and a figure read from a frame depends on picking the settled frame. When a quarter has both a quarter card and a full year card, record both and label them. 2. Write drift_memo.md: the topic, the quarters covered, a table of quarter, quote, link and figure, and three to five lines on what changed in the wording (words that appear, disappear or get softer from one quarter to the next). Quarters with nothing are listed as "not mentioned". The memo records what was said and shown; it gives no opinion on the stock. 3. Show the user the first rows of drift_table.csv, the field mapping above and the full memo. Wait for approval before anything goes into another tool.
In our test run the table had eight rows, each with a figure read from a frame after the count up (one from a half second strip, the other nine from a frame in the middle of the window that already showed the final value); the six quarters that discussed the topic each had a quote checked against the captions, four of them from auto captions and so still NOT VERIFIED by ear; the two year end quarters were marked "not mentioned"; the memo's drift lines were written from the quotes (for example, the word "tariffs" first appears in one quarter, is tied to weak demand in the next two, and is replaced by "trade policy shifts" later).
Done when drift_table.csv has one row per approved quarter, drift_memo.md exists, and the user has approved the sample row and the memo.
Step 8: Optional: put the memo in Notion
Goal: the memo in the user's Notion workspace, only if the user asked for it.
- Writing to Notion acts as the user in a shared tool. Name the destination (workspace and parent page), show the full rendered page, and wait for an explicit yes.
- Create one page with the Notion connector from drift_memo.md, with the table as a Notion table. Links are the user's source links with a time parameter; never a Vivu result page link, which expires after four hours.
- Record the page ID in state.json so a rerun updates the same page instead of creating a second one.
Step 8 was not exercised in our test run: the page was rendered to a local file and nothing was written to Notion.
Done when the user has approved the rendered page and state.json holds the Notion page ID, or the user chose to keep the local files only.
Compliance
- Downloading YouTube videos may conflict with YouTube's Terms of Service. Keep downloads and captions for internal analysis, do not redistribute them, and have the user confirm this is acceptable for their organization before Step 3.
- The people on screen are company executives in videos the company published. The skill identifies no one by face or voice; a speaker's name in the table comes only from the name bar on screen or the transcript's speaker labels, and is marked as such.
- Minors do not appear in results videos. If one ever does, do not index it.
- Where the videos go: indexed videos stay in the user's Vivu project until the user deletes them. The skill creates the project as private and never calls vivu_delete_project or vivu_delete_video unless the user asks, and confirms first.
- Not investment advice. The memo records what was said and shown; recommendations are the user's.
- Before Step 8, confirm the Notion page may hold quotes from the company's videos for internal use.
Known failure modes
| Symptom | Cause | Fix |
|---|---|---|
| (observed) a card frame shows a much lower figure than the company reported | key figure cards count up from a low number, and the frame was taken mid animation | extract frames every half second across the card and read the last one before it fades |
| (observed) the reason text says something the CEO did not say (it turned a line about Europe and North America into a line about North America alone) | the reason paraphrases the moment | quote from the captions or the official transcript only |
| (observed) the topic search skips a short passing mention, often in an opening or closing line | precise search returns the main passage in each video | grep the caption files for the topic words and add those lines as caption check only |
| (observed) the summary section around a real hit has an unrelated title ("Construction Equipment and Buses") | summary sections are chapter level | decide with the captions; use the summary as a second signal only |
| (observed) auto captions write "sick" where the CEO said SEK, and misspell product names | auto captions mishear currencies and names | prefer uploader captions or the official transcript; mark auto caption quotes NOT VERIFIED until heard |
| (observed) "HTTP Error 403: Forbidden" from yt-dlp on a residential IP | YouTube refused the default player client | retry once with the web_embedded player client and check the resolution with ffprobe |
| (observed) "HTTP Error 429: Too Many Requests" while downloading captions | too many caption requests at once | ask for English tracks only and add --sleep-requests and --sleep-subtitles |
| (observed) "HTTP Error 404: Not Found" when listing a channel by its handle | the handle in the URL is not the channel's handle | read the channel ID from one of its videos and list by ID |
| a quarter's video comes from a third party channel | earnings reposts are common on YouTube | use only the company's own channel or IR site |
| a quarter has no topic hit | the CEO did not talk about it that quarter, or the search missed it (an empty result does not prove the topic was never mentioned) | check the captions; if nothing is there, write "not mentioned" |
| the job does not fit the plan | full results webcasts are long | apply the Step 4 levers in order and say what was left out |
| "has not granted vivu.write" | Vivu connected read only | the user reconnects Vivu and allows write access |
| upload page asks to sign in or shows an error | the one time upload link expires after 180 seconds | request a new link right before opening it |
| a file is rejected by the browser upload tool | the file is above the tool's limit (Claude in Chrome takes up to 10 MB per call) | the user opens the upload link in their own browser or adds the file in the Vivu web app; never split or recompress it |
| a table link opens an expired page | a Vivu result page link was used; it expires after four hours | rebuild the link from quarters.csv source_url plus the time parameter |