---
name: competitor-webinar-battlecard
description: "Turn one competitor's YouTube demos and webinars into a Notion battlecard and Linear feature gap issues with Vivu. Use when building or refreshing a battlecard from competitor videos."
---Competitor webinar battlecard with Vivu
This skill turns one competitor's YouTube library (webinar replays, product update videos, demos and how-to videos) into a battlecard page in Notion and one Linear issue per feature gap. Claude lists the channel, pulls the free auto captions, prices the library against the user's Vivu plan and picks the subset worth indexing, downloads and indexes that subset, runs three standing searches, checks every hit against frames or captions, compares the competitor's settings screens with the user's own feature list, and writes the battlecard and the gap issues after the user approves a sample.
The value is in what the competitor shows but never says: the settings form behind a feature that got one sentence in the voiceover (every switch, dropdown and limit the feature has), and the moment a presenter says something is coming soon while the screen shows something else. Captions and titles miss the settings entirely. Pricing pages are collected too, but only as candidates: Vivu sometimes describes a marketing homepage with a Pricing link as a table of plans and prices, so a price enters the battlecard only after Claude has looked at the frames and seen the numbers. Every saved link points to the YouTube video at the right second (&t=SECONDS), so a sales rep can check any battlecard line in one click months later.
When to use
Use when someone asks to build or refresh a battlecard from a competitor's webinars or YouTube channel, list the features and settings a competitor shows in its demos, collect what a competitor says is on its roadmap, or turn competitor videos into feature gap tickets. For a recurring feed of new competitor videos posted to Slack, use the youtube-competitor-watch skill (https://vivu.ai/skills/youtube-competitor-watch) instead. For a one off question about a single video, search Vivu directly. For a pricing comparison alone, read the competitor's pricing page on the web; this skill finds prices in video only when they happen to be on screen.
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. Candidates stay candidates until triaged, and the battlecard marks anything unchecked as UNCERTAIN.
- 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 (the indexed subset, before downloading) or that acts on their behalf (the Notion page, Linear issues, a scheduled refresh).
- Everything saved links to the user's own source: the YouTube URL plus
&t=SECONDS. A Vivu result page link expires after four hours, so it appears only in the live reply. - A roadmap claim is what the competitor said, not a fact. Label it as a claim and quote the captions, never the search reason.
What you need before starting
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, read summaries | vivu_get_account shows can_create_projects: true (tool names may carry a server prefix). A write call failing with "has not granted vivu.write" means the user reconnects Vivu and allows write access |
| A shell on the user's computer with a residential IP, plus a working folder | YouTube blocks downloads from cloud and datacenter IPs; frames are cut from local files | the caption download in Step 2 |
| yt-dlp, ffmpeg with ffprobe, and node | list and download videos, measure minutes, extract frames; yt-dlp needs a JavaScript runtime for YouTube | yt-dlp --version, ffmpeg -version, ffprobe -version, node --version |
| An upload path | move the selected files into Vivu | vivu_open_upload_page plus the user's own browser, or a browser tool that can attach local files |
| The user's own feature list | a gap is only a gap against something | ask for a doc, a features page URL or a pasted list; without it, stop after the battlecard and skip gaps |
| Notion connector with access to the parent page | Step 8 writes the battlecard page | search for the parent page once |
| Linear connector with access to the team | Step 8 creates one issue per gap | list the team's labels once |
This skill needs Claude Code on the user's computer (the terminal or the Code tab of Claude Desktop). Claude on the web and cloud sessions cannot reach the local files or the residential IP.
Inputs to collect
Ask for anything missing, most important first.
- The competitor (COMPETITOR): name and YouTube channel URL. Required.
- Our feature list (OUR_FEATURES). Required for the gap list; without it the run ends at the battlecard.
- Lookback window. Default: the last 12 months of uploads.
- Index minute budget for this run. Default: what vivu_get_usage says remains this month, minus a margin the user picks.
- Notion parent page for the battlecard. Required before Step 8.
- Linear team and label. Default label: competitive-gap.
- Optional: third party review or comparison videos that name the competitor. Official channels rarely show their own pricing page; reviewers sometimes do.
Files and state
Keep everything in one working folder:
battlecard-COMPETITOR/
config.json competitor, channel URL, OUR_FEATURES path, budget, Vivu project id, Notion page, Linear team
videos.tsv one row per listed video: id, upload date, duration, title, caption scores, selected
captions/ auto captions, one .vtt per video
upload/ only the selected .mp4 files
frames/ frames and contact sheets extracted during triage
results/ raw search results, one JSON per query
hits.csv every returned hit: field, youtube_id, start, end, judgment, brand_check, note, frame_file
battlecard.md the rendered battlecard
gaps.csv one row per feature gap
state.json completed steps, uploaded and searched video IDs, Linear issue ID per gap key
A rerun reads state.json first and skips finished steps. A video already in the uploaded list is never uploaded again, and a gap key (competitor plus feature) that already has a Linear issue ID is never filed twice.
Step 1: Check the Vivu connector and the setup
Goal: confirm every requirement before spending anything.
- Call vivu_get_account. If the tool does not exist, tell the user to add the Vivu connector in Claude (
https://mcp.vivu.ai/mcp) and stop. If the account does not showcan_create_projects: true, or a later write call fails with "has not granted vivu.write", ask the user to reconnect Vivu and allow write access, then stop until they have. - Run
yt-dlp --version,ffmpeg -version,ffprobe -versionandnode --version. - Confirm the Notion parent page and the Linear team are reachable.
- Ask for OUR_FEATURES.
Done when vivu_get_account shows can_create_projects: true, the four tool versions print, and the user has either given OUR_FEATURES or agreed to a battlecard without gaps.
Step 2: List the library and pull captions
Goal: know every candidate video and its captions without spending any Vivu allowance.
Ask the user to confirm the Compliance items on downloading before this step.
yt-dlp --flat-playlist --playlist-end 200 --extractor-args youtubetab:approximate_date --print "%(id)s|%(duration)s|%(upload_date)s|%(title)s" "https://www.youtube.com/@HANDLE/videos"
yt-dlp --flat-playlist --playlist-end 200 --extractor-args youtubetab:approximate_date --print "%(id)s|%(duration)s|%(upload_date)s|%(title)s" "https://www.youtube.com/@HANDLE/streams"
yt-dlp --js-runtimes node --skip-download --write-auto-subs --write-subs --sub-langs "en,en-orig" --sub-format vtt --sleep-subtitles 3 -P "home:battlecard-COMPETITOR/captions" -o "%(id)s.%(ext)s" "https://www.youtube.com/watch?v=VIDEO_ID"
HANDLE is the channel handle and VIDEO_ID the 11 character YouTube ID. Webinar replays often live under /streams. approximate_date is coarse: many uploads older than a few months share the same date. Ask only for the caption languages you need; asking for every translated track triggers rate limits.
Done when videos.tsv has one row per listed video and captions/ has a .vtt for each video that has captions.
Step 3: Price the library and pick the subset
Goal: an indexed set the user's plan can pay for, chosen by the user.
A competitor's full library rarely fits a plan. As an estimate, 40 hours of webinars is about 2,400 index minutes; the Premium plan has 180 index minutes a month. Rank the videos with free signals first:
grep -c -i -E "pric|per month|a month|plans? " battlecard-COMPETITOR/captions/VIDEO_ID.en.vtt
grep -c -i -E "coming|upcoming|roadmap|soon|next month|later this year" battlecard-COMPETITOR/captions/VIDEO_ID.en.vtt
grep -c -i -E "settings|set up|configure|enable|turn on|toggle" battlecard-COMPETITOR/captions/VIDEO_ID.en.vtt
Caption counts only rank videos; they never rule one out. In our test run a product update whose captions never mentioned settings still showed a full settings form on screen, and the only competitor pricing page sat in a video whose narration named no price. Rank by video type too (product updates, demos, how-to videos and webinar replays first), then add durations until the budget is reached.
Call vivu_get_usage and show one table:
| This run | Plan allowance | Remaining this month | |
|---|---|---|---|
| Index minutes | sum of selected durations | from vivu_get_usage | from vivu_get_usage |
| Search credits | standing queries x 5, plus rewordings (estimate) | from vivu_get_usage | from vivu_get_usage |
Plan facts: Free is $0 a month with 20 index minutes a month and 50 search credits a month; Premium is $30 a month with 180 index minutes and 500 search credits. A fast search uses 1 credit; a precise search uses 5 credits in total. One battlecard run with the three standing queries uses about 15 search credits (estimate, before any rewording). Whether a larger maximum_results costs more credits is untested.
If the selection does not fit, offer levers in this order: a shorter lookback window, only product update, demo and how-to videos, drop the lowest scoring videos, and last a larger plan. Do not suggest skipping long webinars to save minutes; pricing pages and roadmap talk tend to sit near the end of them.
Done when the user approves the subset and the minute total, and config.json records both.
Step 4: Download, upload and index
Goal: every approved video indexed in a private Vivu project.
yt-dlp --js-runtimes node --restrict-filenames \
-f "bv*[height<=720]+ba/b[height<=720]" --merge-output-format mp4 \
--match-filter "duration<=MAXSECONDS" \
--download-archive battlecard-COMPETITOR/archive.txt \
-P "temp:SCRATCH_DIR_OUTSIDE_UPLOAD" -P "home:battlecard-COMPETITOR/upload" \
-o "%(id)s_%(title).60B.%(ext)s" --retries 5 \
"https://www.youtube.com/watch?v=VIDEO_ID"
MAXSECONDS is the longest video the user approved, in seconds; SCRATCH_DIR_OUTSIDE_UPLOAD is any folder outside upload/. 720p keeps settings labels and prices readable at a fraction of the 1080p size. On HTTP Error 403, retry that video once with --extractor-args "youtube:player_client=web_embedded". If yt-dlp warns that formats were skipped and falls back to 360p, keep the file and tell the user that small labels and billing units may be unreadable. Check each file with ffprobe -v error -show_entries format=duration -of csv=p=0 FILE. The youtube-competitor-watch skill (https://vivu.ai/skills/youtube-competitor-watch) covers channel verification and download troubleshooting in more depth.
- Call vivu_list_projects and reuse a project for this competitor if one exists. Otherwise call vivu_create_project with name "Battlecard COMPETITOR" and visibility "private". The default visibility is organization, which shows the project to everyone in the user's Vivu organization.
- Call vivu_open_upload_page with the project ID immediately before uploading. The link expires in 180 seconds and works once, so never post or store it. Give it to the user to open in their own browser and select the files in upload/, or open it in a browser tool that can attach local files. Claude in Chrome accepts at most 10 MB per upload call; a larger file goes through the user's own browser or the Vivu web app. Never split or recompress a video to fit.
- Vivu replaces spaces and punctuation in file names with underscores, plus signs included. Match results back by the 11 character YouTube ID at the start of each file name.
- Poll vivu_list_videos until every video shows ready, and record each Vivu video_id next to its YouTube ID in state.json.
In our test run the 8 videos (29.59 minutes) were ready 448 seconds after the upload started, about 7.5 minutes. The files were uploaded through the same upload page by an automated browser; opening the link in the user's own browser, as item 2 describes, was not exercised in our test run.
Done when every approved video shows ready and state.json maps each YouTube ID to its Vivu video_id.
Step 5: Run the standing queries
Goal: candidate moments for each battlecard block.
| Field | Query | Mode | maximum_results | Role |
|---|---|---|---|---|
| roadmap_said | the speaker says out loud that a feature is not released yet: it is coming soon, upcoming, or planned for the coming months (spoken in the voiceover, not only written on screen) | precise | 20 | core: Roadmap claims |
| settings_panel | a settings or configuration panel for a product feature with toggles, dropdowns or form fields visible on screen | precise | 40 | core: Feature inventory and gaps |
| pricing_candidate | a pricing table on screen where each plan column shows a price amount, a number with a currency symbol per month, next to the plan name | precise | 20 | candidates only, see Step 6 |
The chain: roadmap_said finds what the competitor says it does not have yet; settings_panel flips from said to shown and lays out what it has already shipped, screen by screen, which is what the gap list compares against; pricing_candidate adds which plan those screens sit behind, when a plans page happens to be on screen. All three are precise because only precise returns a time window and a reason; fast returns whole files with an empty reason, and vivu_search_videos has no file filter that would let fast narrow the precise search.
maximum_results is also the recall ceiling. In our test run the settings query at maximum_results 20 returned 20, exactly the cap, so it was rerun at 40. The roadmap and pricing queries returned far fewer than 20.
If a settings run returns exactly its cap, raise maximum_results and rerun before building the inventory; maximum_results is 1 to 100.
How far to trust each field: finding spoken statements by meaning is reliable in Vivu, though the reason text is a paraphrase, not a transcript. Matching what a screen looks like (a settings form) is rated weak in general because it can miss moments, and this skill's settings query has been measured on one test corpus only. Reading prices from a screen is the weakest of the three here, so pricing hits are candidates. No result goes into the battlecard on the search alone: Step 6 checks every hit against frames or captions.
Run each query with vivu_search_videos (project_id, query, mode, maximum_results). 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 each completed result to results/FIELD.json. Show the Vivu result page link in the live reply; it expires after four hours, so it never goes into a file, the battlecard or an issue.
Worked example from our test run
The test corpus was public: 8 videos (29.59 minutes) from webinar software vendors' official channels (a how-to, three monthly product updates, a talk about paid webinars, a partner integration tutorial, a short demo) plus one third party comparison video.
- roadmap_said: the wording in the table came after 1 rewording on the same corpus. It returned 3 moments, 3 real, 0 false and 0 missed against the 3 roadmap statements in the captions. The first wording also returned an on screen "(Feature in beta)" label and a "coming soon" the reason made up, 2 false out of 5.
- settings_panel: at maximum_results 40 it returned 24 moments, 22 real and 2 false, a false positive rate of 8%. The false ones were an AI generated mock interface in the comparison video and an intro animation. Rerunning at the higher cap found 3 more real settings screens than the run capped at 20. No full ground truth exists for this field, so recall is unknown. Several real moments were settings screens of a partner or another vendor, which the brand check keeps out.
- pricing_candidate: the wording in the table came after 2 rewordings on the same corpus, and all three wordings behaved the same: 3 moments, 2 real pricing pages and 1 false, 0 missed. The false one was two vendor homepages with a Pricing link and no price; the reason described plans with monthly prices. That is why every pricing candidate goes through the contact sheet in Step 6.
Done when every standing query has a completed result saved in results/ and no settings run returned exactly its maximum_results.
Step 6: Triage every hit
Goal: every hit judged real or false against the video, with the frame or caption that proves it.
For settings panels, extract three frames inside the window (start plus half a second, middle, end minus half a second) and a strip of the three side by side:
ffmpeg -v error -y -ss START -i FILE -ss MID -i FILE -ss END -i FILE -filter_complex "[0:v]split=2[s0][t0];[1:v]split=2[s1][t1];[2:v]split=2[s2][t2];[t0]scale=640:-2[u0];[t1]scale=640:-2[u1];[t2]scale=640:-2[u2];[u0][u1][u2]hstack=3[strip]" -map "[s0]" -frames:v 1 frames/FIELD_hN_start.png -map "[s1]" -frames:v 1 frames/FIELD_hN_mid.png -map "[s2]" -frames:v 1 frames/FIELD_hN_end.png -map "[strip]" -frames:v 1 frames/FIELD_hN_strip.png
START, MID and END are seconds, FILE is the local video, FIELD and N name the hit. Look at the strip. A hit is real when at least one frame shows a settings or configuration form (switches, dropdowns, input fields) for a product feature. A marketing page, a mock illustration or the presenter talking is false. If none of the three frames shows the target, tile the window at one frame per second before calling it false, because a form can sit between the sampled frames:
ffmpeg -v error -y -ss START -to END -i FILE -vf "fps=1,scale=480:-2,tile=4x4" -frames:v 1 frames/FIELD_hN_every_second.png
Then run the brand check: whose product is on screen? Partner tools and other vendors' screens are real Vivu hits but stay out of this competitor's inventory.
For pricing candidates, a price enters the battlecard only after a contact sheet of the whole window at two frames per second shows a plans table with prices:
ffmpeg -v error -y -ss START -to END -i FILE -vf "fps=2,scale=320:-2,tile=8x6" -frames:v 1 frames/pricing_hN_every_half_second.png
Raise the tile rows for windows longer than 24 seconds. Read plan names and prices from the frames, never from the reason. Record whose pricing page it is. A candidate with no price in any tile is rejected and counted.
In our test run the reason quoted a price that differs from the one in the frame, and it described plans and prices on a homepage that had none.
For roadmap_said, quote from the local captions (captions/VIDEO_ID.en.vtt) around the window, never from the reason, which paraphrases and can invent a phrase. Use vivu_get_video_summary with include_segments, start_ms and end_ms around the hit as a second signal. "You can now" and "recently released" are shipped features, not roadmap; a word written only on screen is not a spoken claim.
Write every hit to hits.csv with its judgment and frame file, and tell the user the false positive count per field. Anything not checked stays labeled as a candidate.
Done when every row of hits.csv has a judgment of real, false or rejected and each real visual hit names a frame file that exists.
Step 7: Build the battlecard and the gap list
Goal: a battlecard and gap list the user has approved in sample form.
Render battlecard.md with these blocks, each line ending in its YouTube link:
# COMPETITOR battlecard (DATE, from N videos, M minutes indexed)
## Roadmap claims (what they say is coming; claims, not facts)
- "QUOTE FROM CAPTIONS" (VIDEO TITLE, MM:SS, https://www.youtube.com/watch?v=VIDEO_ID&t=SECONDS)
## Feature inventory from settings panels
- FEATURE: SETTING, SETTING, SETTING (VIDEO TITLE, MM:SS, LINK)
## Pricing (only candidates confirmed on a contact sheet)
- PLAN: PRICE per UNIT (billing period NOT VERIFIED if unreadable) (VIDEO TITLE, MM:SS, LINK)
- Rejected candidates: N, and why
## Gaps vs OUR_PRODUCT
- FEATURE: missing | partial (UNCERTAIN until a product owner confirms)
## Sources
- VIDEO TITLE, https://www.youtube.com/watch?v=VIDEO_ID
When no pricing candidate survives the contact sheet, write "No pricing page confirmed in the indexed videos" rather than leaving the block out, and say that this is not proof none exists.
Compare each inventory line with OUR_FEATURES and write gaps.csv:
competitor,feature,evidence_type,video_id,title,timestamp,youtube_link,frame_file,our_status,priority_guess
COMPETITOR,Caption hints for brand names,settings_panel,VIDEO_ID,Product update,00:44,https://www.youtube.com/watch?v=VIDEO_ID&t=44,settings_panel_h6_mid.png,missing,UNCERTAIN
| Column | Source | If unavailable |
|---|---|---|
| feature | the settings screen in the frame | drop the row |
| video_id, title, timestamp, youtube_link | hits.csv and videos.tsv | drop the row |
| frame_file | the triage frame that shows the setting | drop the row |
| our_status | Claude's reading of OUR_FEATURES (inferred) | UNCERTAIN |
| priority_guess | left for the user | UNCERTAIN |
our_status is an inference from the list the user gave. If it is wrong, the Linear issue describes a gap that does not exist, so every row stays UNCERTAIN until the user or a product owner confirms it. Show the user the battlecard outline, one gaps.csv row and the mapping table, and wait for approval before bulk work. If fewer gaps are found than the user expects, report the real number.
Done when battlecard.md and gaps.csv exist and the user has approved the sample row and the mapping.
Step 8: Write the battlecard to Notion and the gaps to Linear
Goal: the approved battlecard and issues in the user's tools. This step was not exercised in our test run; the page and issues were only rendered to files.
Writing to Notion and Linear acts as the user. Name the Notion parent page and the Linear team, show one fully rendered issue and the page outline, and wait for an explicit yes.
- Create one Notion page under the parent page with the content of battlecard.md.
- For each gaps.csv row without an issue ID in state.json, create one Linear issue: title "[Gap] COMPETITOR: FEATURE", description with one sentence, the YouTube link with
&t=SECONDS, the frame file name, our_status and a link to the Notion page, label competitive-gap, no assignee. Record the issue ID in state.json.
Sample issue:
[Gap] COMPETITOR: Caption hints for brand names
COMPETITOR shows caption hints in Product update at 00:44 (https://www.youtube.com/watch?v=VIDEO_ID&t=44). Evidence: settings panel, frame settings_panel_h6_mid.png. Our status: missing (UNCERTAIN). Battlecard: NOTION_PAGE_LINK.
Done when the Notion page exists and every gaps.csv row has a Linear issue ID in state.json.
Step 9: Refresh each quarter
Goal: an updated battlecard without redoing old videos. This step was not exercised in our test run.
A refresh needs the shell, the local folder and the residential IP, so it runs as a local routine in Claude Code Desktop (Code tab, Routines, New routine, Local). It runs only while Claude Desktop is open and the computer is awake. The routine prompt must stand alone: the working folder path, config.json, the Vivu project ID, the Notion page, the Linear team, and these steps: list new uploads since the last run, repeat Steps 3 to 7 for new videos only (videos already in state.json are skipped; the search covers the whole project, so keep only hits on new video IDs), then pause for approval before Step 8. Show the user the prompt and the cadence, wait for approval, create it, and start it once with Run now so the user can allow its tools.
Done when the routine exists, the user approved its prompt and cadence, and one Run now has finished.
Compliance
- Downloading YouTube videos may conflict with YouTube's Terms of Service. Keep downloads for internal competitive analysis, do not redistribute them, and do not use the competitor's footage or logos in anything public. Have the user confirm this is acceptable for their organization before Step 2.
- The people on screen are the competitor's staff in public marketing videos. Do not identify them; the battlecard names companies and videos, not presenters. Do not download videos centered on minors.
- The videos stay in the user's Vivu project until the user deletes them. Create the project as private; the default is visible to the whole Vivu organization. Delete videos or the project only when the user asks, and confirm first.
- Steps 8 and 9 act on the user's behalf in Notion, Linear and a local routine; each needs the approval described there.
- This skill does no face, logo or identity recognition.
Known failure modes
| Symptom | Cause | Fix |
|---|---|---|
| (observed) "ERROR: unable to download video data: HTTP Error 403: Forbidden" | YouTube refused the default yt-dlp client | retry that video once with the web_embedded player client (Step 4) |
| (observed) "Some web_embedded client https formats have been skipped as they are missing a URL", then a 360p file | YouTube's SABR streaming experiment | keep the file, warn that small labels and billing units may be unreadable |
| (observed) "HTTP Error 429: Too Many Requests" on caption downloads | too many caption tracks requested at once | ask only for en and en-orig and keep the sleep between caption requests (Step 2) |
| (observed) a marketing homepage comes back as a pricing page with made up plans and prices | the search matched a Pricing link or narration about pricing | reject any pricing candidate whose contact sheet shows no price |
| (observed) the settings query returns exactly maximum_results moments | the cap, not the footage, set the count | raise maximum_results and rerun before building the inventory |
| (observed) none of the sampled frames shows the settings form, yet the form is in the window | the window is longer than the time the form is on screen | tile the window at a frame per second before calling it false |
| (observed) a word written on screen, such as a beta label, comes back as a spoken roadmap claim | speech and screen text mixed in the match | keep the roadmap wording that asks for spoken statements only; check captions |
| (observed) a settings hit shows a partner's or another vendor's screen | tutorials and comparison videos show several products | brand check each hit before it enters the inventory |
| 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 tool accepts at most 10 MB per call | the user adds that file in their own browser or the Vivu web app |
| "has not granted vivu.write" | Vivu connected read only | the user reconnects Vivu with write access |
| a result's file name does not match videos.tsv | Vivu normalized the file name | match on the YouTube ID at the start of the name |
| "Sign in to confirm you're not a bot" | download attempted from a cloud IP | run the downloads on the user's computer |
| an empty settings or roadmap result | the indexed videos may not contain it, or the query missed it | an empty result does not prove the footage has none; try a broader wording or more videos |