mcp-scraper 0.4.12 → 0.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/api-server.cjs +240 -218
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +1 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/{chunk-2YY46QYT.js → chunk-AUCXKRRH.js} +3 -1
- package/dist/chunk-OL65BO4S.js +7 -0
- package/dist/chunk-OL65BO4S.js.map +1 -0
- package/dist/{chunk-EMY7ELRU.js → chunk-SIPXEFTR.js} +14 -10
- package/dist/chunk-SIPXEFTR.js.map +1 -0
- package/dist/{chunk-ONIOF5XW.js → chunk-VQV7MB2S.js} +2 -2
- package/dist/{chunk-6EXP6DQG.js → chunk-XZ2GPRCM.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{server-UNID3SJU.js → server-TGORO4AJ.js} +219 -208
- package/dist/server-TGORO4AJ.js.map +1 -0
- package/dist/{site-extract-repository-HTIY52MW.js → site-extract-repository-6X2C7RLF.js} +3 -3
- package/dist/{worker-HTYZAYGB.js → worker-WCSKUAOT.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-EMY7ELRU.js.map +0 -1
- package/dist/chunk-SHXJQQOH.js +0 -7
- package/dist/chunk-SHXJQQOH.js.map +0 -1
- package/dist/server-UNID3SJU.js.map +0 -1
- /package/dist/{chunk-2YY46QYT.js.map → chunk-AUCXKRRH.js.map} +0 -0
- /package/dist/{chunk-ONIOF5XW.js.map → chunk-VQV7MB2S.js.map} +0 -0
- /package/dist/{chunk-6EXP6DQG.js.map → chunk-XZ2GPRCM.js.map} +0 -0
- /package/dist/{site-extract-repository-HTIY52MW.js.map → site-extract-repository-6X2C7RLF.js.map} +0 -0
- /package/dist/{worker-HTYZAYGB.js.map → worker-WCSKUAOT.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -7986,7 +7986,7 @@ function insufficientBalanceResponse(balanceMc, requiredMc) {
|
|
|
7986
7986
|
topup_url: topupUrl
|
|
7987
7987
|
};
|
|
7988
7988
|
}
|
|
7989
|
-
var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, SCHEDULING_PLANS, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, SITE_AUDIT_LLM_MARGIN_MULTIPLE, VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE, MC_PER_USD;
|
|
7989
|
+
var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, SCHEDULING_PLANS, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, SITE_AUDIT_LLM_MARGIN_MULTIPLE, VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_HOLD_MC, MC_PER_USD;
|
|
7990
7990
|
var init_rates = __esm({
|
|
7991
7991
|
"src/api/rates.ts"() {
|
|
7992
7992
|
"use strict";
|
|
@@ -8003,13 +8003,13 @@ var init_rates = __esm({
|
|
|
8003
8003
|
maps_place: 6e3,
|
|
8004
8004
|
maps_review: 100,
|
|
8005
8005
|
fb_search: 600,
|
|
8006
|
-
fb_transcribe:
|
|
8006
|
+
fb_transcribe: 33300,
|
|
8007
8007
|
google_ads_search: 600,
|
|
8008
8008
|
google_ads_intel: 200,
|
|
8009
|
-
google_ads_transcribe:
|
|
8009
|
+
google_ads_transcribe: 33300,
|
|
8010
8010
|
instagram_profile: 400,
|
|
8011
8011
|
instagram_media: 400,
|
|
8012
|
-
instagram_transcribe:
|
|
8012
|
+
instagram_transcribe: 33300,
|
|
8013
8013
|
browser_minute: 12e3,
|
|
8014
8014
|
reddit_thread: 3e3,
|
|
8015
8015
|
reddit_comment: 200,
|
|
@@ -8131,16 +8131,16 @@ var init_rates = __esm({
|
|
|
8131
8131
|
label: "Google ad video transcription",
|
|
8132
8132
|
aliases: ["google_ads_transcribe", "google ad transcript"],
|
|
8133
8133
|
credits: mcToCredits(MC_COSTS.google_ads_transcribe),
|
|
8134
|
-
unit: "per minute",
|
|
8135
|
-
notes: "A hold is taken, then reconciled to actual
|
|
8134
|
+
unit: "per minute (typical)",
|
|
8135
|
+
notes: "A hold is taken, then reconciled to the real transcription cost for the actual audio duration."
|
|
8136
8136
|
},
|
|
8137
8137
|
{
|
|
8138
8138
|
key: "fb_transcribe",
|
|
8139
8139
|
label: "Facebook video / ad transcription",
|
|
8140
8140
|
aliases: ["facebook_transcribe", "facebook_video_transcribe", "fb_transcribe", "fb ad transcript"],
|
|
8141
8141
|
credits: mcToCredits(MC_COSTS.fb_transcribe),
|
|
8142
|
-
unit: "per minute",
|
|
8143
|
-
notes: "A hold is taken, then reconciled to actual
|
|
8142
|
+
unit: "per minute (typical)",
|
|
8143
|
+
notes: "A hold is taken, then reconciled to the real transcription cost for the actual audio duration."
|
|
8144
8144
|
},
|
|
8145
8145
|
{
|
|
8146
8146
|
key: "instagram_profile",
|
|
@@ -8163,8 +8163,8 @@ var init_rates = __esm({
|
|
|
8163
8163
|
label: "Instagram media transcription",
|
|
8164
8164
|
aliases: ["instagram transcript", "instagram reel transcript", "ig transcribe"],
|
|
8165
8165
|
credits: mcToCredits(MC_COSTS.instagram_transcribe),
|
|
8166
|
-
unit: "per
|
|
8167
|
-
notes: "Whisper transcription of selected Instagram audio/video media via fal.ai."
|
|
8166
|
+
unit: "per minute (typical)",
|
|
8167
|
+
notes: "Whisper transcription of selected Instagram audio/video media via fal.ai. A hold is taken, then reconciled to the real transcription cost for the actual audio duration."
|
|
8168
8168
|
},
|
|
8169
8169
|
{
|
|
8170
8170
|
key: "browser_minute",
|
|
@@ -8333,6 +8333,8 @@ var init_rates = __esm({
|
|
|
8333
8333
|
MEMORY_AI_MARGIN_MULTIPLE = 3;
|
|
8334
8334
|
SITE_AUDIT_LLM_MARGIN_MULTIPLE = 1.5;
|
|
8335
8335
|
VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE = 1.5;
|
|
8336
|
+
MEDIA_TRANSCRIBE_MARGIN_MULTIPLE = 3;
|
|
8337
|
+
MEDIA_TRANSCRIBE_HOLD_MC = MC_COSTS.fb_transcribe * 3;
|
|
8336
8338
|
MC_PER_USD = SUBSCRIPTION_TIERS["price_1TmiHRS8aAcsk3TGwmSNfNIa"].credits_mc / SUBSCRIPTION_TIERS["price_1TmiHRS8aAcsk3TGwmSNfNIa"].monthly_usd;
|
|
8337
8339
|
}
|
|
8338
8340
|
});
|
|
@@ -13537,6 +13539,104 @@ var init_kernel_proxy_resolver = __esm({
|
|
|
13537
13539
|
}
|
|
13538
13540
|
});
|
|
13539
13541
|
|
|
13542
|
+
// src/youtube/audio-resolver.ts
|
|
13543
|
+
function mediaResolverUrl() {
|
|
13544
|
+
return process.env.FRAMES_SERVICE_URL?.trim() || void 0;
|
|
13545
|
+
}
|
|
13546
|
+
function mediaResolverSecret() {
|
|
13547
|
+
return process.env.FRAMES_SERVICE_SECRET?.trim() || void 0;
|
|
13548
|
+
}
|
|
13549
|
+
function mediaResolverConfigured() {
|
|
13550
|
+
return Boolean(mediaResolverUrl() && mediaResolverSecret());
|
|
13551
|
+
}
|
|
13552
|
+
async function resolveYouTubeAudio(videoId) {
|
|
13553
|
+
const base = mediaResolverUrl();
|
|
13554
|
+
const secret2 = mediaResolverSecret();
|
|
13555
|
+
if (!base || !secret2) return null;
|
|
13556
|
+
const controller = new AbortController();
|
|
13557
|
+
const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
|
|
13558
|
+
try {
|
|
13559
|
+
const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
|
|
13560
|
+
method: "POST",
|
|
13561
|
+
headers: { "content-type": "application/json", "x-frames-secret": secret2 },
|
|
13562
|
+
body: JSON.stringify({ videoUrl: `https://www.youtube.com/watch?v=${videoId}` }),
|
|
13563
|
+
signal: controller.signal
|
|
13564
|
+
});
|
|
13565
|
+
if (!resp.ok) {
|
|
13566
|
+
const detail = await resp.text().catch(() => "");
|
|
13567
|
+
console.warn(`[audio-resolver] ${videoId}: http_${resp.status} ${detail.slice(0, 200)}`);
|
|
13568
|
+
return null;
|
|
13569
|
+
}
|
|
13570
|
+
const data = await resp.json();
|
|
13571
|
+
if (!data.mediaUrl) {
|
|
13572
|
+
console.warn(`[audio-resolver] ${videoId}: response missing mediaUrl`);
|
|
13573
|
+
return null;
|
|
13574
|
+
}
|
|
13575
|
+
return {
|
|
13576
|
+
mediaUrl: data.mediaUrl,
|
|
13577
|
+
durationS: Number(data.durationS) || 0,
|
|
13578
|
+
sizeBytes: Number(data.sizeBytes) || 0,
|
|
13579
|
+
mimeType: data.mimeType ?? "audio/mp4"
|
|
13580
|
+
};
|
|
13581
|
+
} catch (err) {
|
|
13582
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13583
|
+
console.warn(`[audio-resolver] ${videoId}: resolve failed \u2014 ${msg}`);
|
|
13584
|
+
return null;
|
|
13585
|
+
} finally {
|
|
13586
|
+
clearTimeout(timer);
|
|
13587
|
+
}
|
|
13588
|
+
}
|
|
13589
|
+
var RESOLVE_TIMEOUT_MS;
|
|
13590
|
+
var init_audio_resolver = __esm({
|
|
13591
|
+
"src/youtube/audio-resolver.ts"() {
|
|
13592
|
+
"use strict";
|
|
13593
|
+
RESOLVE_TIMEOUT_MS = 24e4;
|
|
13594
|
+
}
|
|
13595
|
+
});
|
|
13596
|
+
|
|
13597
|
+
// src/services/media-transcription.ts
|
|
13598
|
+
function transcriptMarkdown(title, text, chunks, durationMs) {
|
|
13599
|
+
const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
|
13600
|
+
const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
|
|
13601
|
+
if (chunks.length) {
|
|
13602
|
+
lines.push("## Timestamped Segments", "");
|
|
13603
|
+
for (const ch of chunks) {
|
|
13604
|
+
lines.push(`**[${fmtTs2(ch.timestamp[0])} -> ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
|
|
13605
|
+
}
|
|
13606
|
+
}
|
|
13607
|
+
return lines.join("\n");
|
|
13608
|
+
}
|
|
13609
|
+
async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
|
|
13610
|
+
const startMs = Date.now();
|
|
13611
|
+
const result = await import_client3.fal.subscribe("fal-ai/wizper", {
|
|
13612
|
+
input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
|
|
13613
|
+
logs: false,
|
|
13614
|
+
pollInterval: 3e3
|
|
13615
|
+
});
|
|
13616
|
+
const data = result.data;
|
|
13617
|
+
const text = data.text ?? "";
|
|
13618
|
+
const chunks = data.chunks ?? [];
|
|
13619
|
+
const durationMs = Date.now() - startMs;
|
|
13620
|
+
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
|
|
13621
|
+
const costUsd = vendorCostUsd("fal_wizper", durationMs / 1e3);
|
|
13622
|
+
return {
|
|
13623
|
+
text,
|
|
13624
|
+
chunks,
|
|
13625
|
+
durationMs,
|
|
13626
|
+
costUsd,
|
|
13627
|
+
markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
|
|
13628
|
+
};
|
|
13629
|
+
}
|
|
13630
|
+
var import_client3;
|
|
13631
|
+
var init_media_transcription = __esm({
|
|
13632
|
+
"src/services/media-transcription.ts"() {
|
|
13633
|
+
"use strict";
|
|
13634
|
+
import_client3 = require("@fal-ai/client");
|
|
13635
|
+
init_cost_telemetry();
|
|
13636
|
+
init_cost_rates();
|
|
13637
|
+
}
|
|
13638
|
+
});
|
|
13639
|
+
|
|
13540
13640
|
// src/youtube/CaptionFetcher.ts
|
|
13541
13641
|
async function fetchViaYoutubeTranscript(videoId) {
|
|
13542
13642
|
try {
|
|
@@ -13605,13 +13705,12 @@ function parseTimedtextXml(xml) {
|
|
|
13605
13705
|
}
|
|
13606
13706
|
return results;
|
|
13607
13707
|
}
|
|
13608
|
-
function isBotBlockStatus(status) {
|
|
13609
|
-
return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
|
|
13610
|
-
}
|
|
13611
13708
|
async function probePlayer(page, videoId) {
|
|
13612
13709
|
return page.evaluate(
|
|
13613
13710
|
async ({ vid, clients }) => {
|
|
13614
13711
|
let lastStatus = null;
|
|
13712
|
+
let sawBotBlock = false;
|
|
13713
|
+
let sawAccessible = false;
|
|
13615
13714
|
for (const client2 of clients) {
|
|
13616
13715
|
const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
|
|
13617
13716
|
method: "POST",
|
|
@@ -13627,97 +13726,24 @@ async function probePlayer(page, videoId) {
|
|
|
13627
13726
|
}
|
|
13628
13727
|
const data = await resp.json();
|
|
13629
13728
|
lastStatus = data?.playabilityStatus?.status ?? null;
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
let captions = null;
|
|
13729
|
+
if (lastStatus === "LOGIN_REQUIRED" || lastStatus === "AGE_VERIFICATION_REQUIRED") sawBotBlock = true;
|
|
13730
|
+
else if (lastStatus === "OK") sawAccessible = true;
|
|
13633
13731
|
const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
13634
13732
|
if (tracks.length > 0) {
|
|
13635
13733
|
const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
|
|
13636
13734
|
const sep = track.baseUrl.includes("?") ? "&" : "?";
|
|
13637
13735
|
const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
|
|
13638
|
-
if (xmlResp?.ok)
|
|
13736
|
+
if (xmlResp?.ok) {
|
|
13737
|
+
const captions = await xmlResp.text();
|
|
13738
|
+
if (captions) return { captions, status: lastStatus, escalate: false };
|
|
13739
|
+
}
|
|
13639
13740
|
}
|
|
13640
|
-
if (!captions && !pick) continue;
|
|
13641
|
-
return {
|
|
13642
|
-
captions,
|
|
13643
|
-
status: lastStatus,
|
|
13644
|
-
audio: pick ? {
|
|
13645
|
-
url: pick.url,
|
|
13646
|
-
mimeType: pick.mimeType,
|
|
13647
|
-
bitrate: pick.bitrate ?? null,
|
|
13648
|
-
contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
|
|
13649
|
-
approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
|
|
13650
|
-
} : null
|
|
13651
|
-
};
|
|
13652
13741
|
}
|
|
13653
|
-
return { captions: null,
|
|
13742
|
+
return { captions: null, status: lastStatus, escalate: sawBotBlock && !sawAccessible };
|
|
13654
13743
|
},
|
|
13655
13744
|
{ vid: videoId, clients: INNERTUBE_CLIENTS }
|
|
13656
13745
|
).catch(() => null);
|
|
13657
13746
|
}
|
|
13658
|
-
async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
|
|
13659
|
-
if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
|
|
13660
|
-
console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
|
|
13661
|
-
return null;
|
|
13662
|
-
}
|
|
13663
|
-
const sep = audio.url.includes("?") ? "&" : "?";
|
|
13664
|
-
const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
|
|
13665
|
-
const result = await page.evaluate(async (u) => {
|
|
13666
|
-
try {
|
|
13667
|
-
const resp = await fetch(u);
|
|
13668
|
-
if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
|
|
13669
|
-
const blob = await resp.blob();
|
|
13670
|
-
const dataUrl = await new Promise((resolve, reject) => {
|
|
13671
|
-
const reader = new FileReader();
|
|
13672
|
-
reader.onload = () => resolve(reader.result);
|
|
13673
|
-
reader.onerror = () => reject(reader.error);
|
|
13674
|
-
reader.readAsDataURL(blob);
|
|
13675
|
-
});
|
|
13676
|
-
return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
|
|
13677
|
-
} catch (e) {
|
|
13678
|
-
return { ok: false, error: String(e) };
|
|
13679
|
-
}
|
|
13680
|
-
}, url).catch((e) => ({ ok: false, error: String(e) }));
|
|
13681
|
-
if (!result.ok || !result.base64) {
|
|
13682
|
-
console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
|
|
13683
|
-
return null;
|
|
13684
|
-
}
|
|
13685
|
-
const bytes = Buffer.from(result.base64, "base64");
|
|
13686
|
-
if (audio.contentLength && bytes.length < audio.contentLength) {
|
|
13687
|
-
console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
|
|
13688
|
-
return null;
|
|
13689
|
-
}
|
|
13690
|
-
return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
|
|
13691
|
-
}
|
|
13692
|
-
async function transcribeWithWizper(videoId, audio, startMs) {
|
|
13693
|
-
const falKey = process.env.FAL_KEY;
|
|
13694
|
-
if (!falKey) return null;
|
|
13695
|
-
try {
|
|
13696
|
-
import_client3.fal.config({ credentials: falKey });
|
|
13697
|
-
const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
|
|
13698
|
-
const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
|
|
13699
|
-
const uploadedUrl = await import_client3.fal.storage.upload(audioFile);
|
|
13700
|
-
const subscribeStartMs = Date.now();
|
|
13701
|
-
const result = await import_client3.fal.subscribe("fal-ai/wizper", {
|
|
13702
|
-
input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
|
|
13703
|
-
logs: false,
|
|
13704
|
-
pollInterval: 3e3
|
|
13705
|
-
});
|
|
13706
|
-
const subscribeDurationMs = Date.now() - subscribeStartMs;
|
|
13707
|
-
const data = result.data;
|
|
13708
|
-
const chunks = (data.chunks ?? []).map((c) => ({
|
|
13709
|
-
timestamp: c.timestamp,
|
|
13710
|
-
text: c.text.trim()
|
|
13711
|
-
}));
|
|
13712
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
|
|
13713
|
-
const text = data.text ?? chunks.map((c) => c.text).join(" ");
|
|
13714
|
-
if (!text) return null;
|
|
13715
|
-
return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
|
|
13716
|
-
} catch (err) {
|
|
13717
|
-
console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13718
|
-
return null;
|
|
13719
|
-
}
|
|
13720
|
-
}
|
|
13721
13747
|
async function resolveResidentialProxy(kernelApiKey) {
|
|
13722
13748
|
try {
|
|
13723
13749
|
const resolved = await resolveKernelProxyId({
|
|
@@ -13733,14 +13759,13 @@ async function resolveResidentialProxy(kernelApiKey) {
|
|
|
13733
13759
|
return {};
|
|
13734
13760
|
}
|
|
13735
13761
|
}
|
|
13736
|
-
async function
|
|
13762
|
+
async function probeCaptions(videoId, kernelProxyIdOverride) {
|
|
13737
13763
|
const kernelApiKey = browserServiceApiKey();
|
|
13738
13764
|
if (!kernelApiKey) return null;
|
|
13739
|
-
const kernelProxyId =
|
|
13765
|
+
const kernelProxyId = kernelProxyIdOverride ?? browserServiceProxyId();
|
|
13740
13766
|
const driver = new BrowserDriver();
|
|
13741
13767
|
return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
|
|
13742
13768
|
let entries = [];
|
|
13743
|
-
let audio = null;
|
|
13744
13769
|
let retryWithFreshIp = false;
|
|
13745
13770
|
try {
|
|
13746
13771
|
await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
|
|
@@ -13752,69 +13777,73 @@ async function probeAndExtract(videoId, opts) {
|
|
|
13752
13777
|
);
|
|
13753
13778
|
const probe = await probePlayer(page, videoId);
|
|
13754
13779
|
if (!probe) return null;
|
|
13755
|
-
if (probe.captions
|
|
13780
|
+
if (probe.captions) {
|
|
13756
13781
|
entries = parseTimedtextJson3(probe.captions);
|
|
13757
13782
|
if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
|
|
13758
13783
|
}
|
|
13759
13784
|
if (entries.length === 0) {
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
|
|
13763
|
-
return { entries, audio: null, retryWithFreshIp };
|
|
13764
|
-
}
|
|
13765
|
-
audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
|
|
13766
|
-
if (!audio) return { entries, audio: null, retryWithFreshIp: true };
|
|
13785
|
+
retryWithFreshIp = probe.escalate;
|
|
13786
|
+
console.warn(`[CaptionFetcher] no caption track for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)}, escalate=${probe.escalate})`);
|
|
13767
13787
|
}
|
|
13768
13788
|
} catch (err) {
|
|
13769
13789
|
console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13770
13790
|
return null;
|
|
13771
13791
|
} finally {
|
|
13772
|
-
|
|
13773
|
-
await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
|
|
13792
|
+
await driver.close();
|
|
13774
13793
|
}
|
|
13775
|
-
return { entries,
|
|
13794
|
+
return { entries, retryWithFreshIp };
|
|
13776
13795
|
});
|
|
13777
13796
|
}
|
|
13778
|
-
async function extractWithProxyEscalation(videoId, opts) {
|
|
13779
|
-
const direct = await probeAndExtract(videoId, opts);
|
|
13780
|
-
if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
|
|
13781
|
-
const shouldEscalate = direct === null || direct.retryWithFreshIp;
|
|
13782
|
-
if (!shouldEscalate) return direct;
|
|
13783
|
-
const kernelApiKey = browserServiceApiKey();
|
|
13784
|
-
if (!kernelApiKey) return direct;
|
|
13785
|
-
const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
|
|
13786
|
-
if (!kernelProxyId) return direct;
|
|
13787
|
-
try {
|
|
13788
|
-
console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
|
|
13789
|
-
const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
|
|
13790
|
-
return viaProxy ?? direct;
|
|
13791
|
-
} finally {
|
|
13792
|
-
if (disposableProxyId) {
|
|
13793
|
-
await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
|
|
13794
|
-
(err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
|
|
13795
|
-
);
|
|
13796
|
-
}
|
|
13797
|
-
}
|
|
13798
|
-
}
|
|
13799
13797
|
async function fetchViaKernelSession(videoId) {
|
|
13800
13798
|
const start = Date.now();
|
|
13801
|
-
|
|
13802
|
-
if (!outcome)
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13799
|
+
let outcome = await probeCaptions(videoId);
|
|
13800
|
+
if (!outcome || outcome.entries.length === 0 && outcome.retryWithFreshIp) {
|
|
13801
|
+
const kernelApiKey = browserServiceApiKey();
|
|
13802
|
+
if (kernelApiKey) {
|
|
13803
|
+
const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
|
|
13804
|
+
if (kernelProxyId) {
|
|
13805
|
+
try {
|
|
13806
|
+
console.warn(`[CaptionFetcher] ${videoId}: retrying captions through residential proxy after bot block`);
|
|
13807
|
+
outcome = await probeCaptions(videoId, kernelProxyId) ?? outcome;
|
|
13808
|
+
} finally {
|
|
13809
|
+
if (disposableProxyId) {
|
|
13810
|
+
await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
|
|
13811
|
+
(err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
|
|
13812
|
+
);
|
|
13813
|
+
}
|
|
13814
|
+
}
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13816
13817
|
}
|
|
13817
|
-
return null;
|
|
13818
|
+
if (!outcome || outcome.entries.length === 0) return null;
|
|
13819
|
+
const chunks = outcome.entries.map((e) => ({
|
|
13820
|
+
timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
|
|
13821
|
+
text: e.text
|
|
13822
|
+
}));
|
|
13823
|
+
const text = chunks.map((c) => c.text).join(" ");
|
|
13824
|
+
return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
|
|
13825
|
+
}
|
|
13826
|
+
async function fetchViaResolvedAudio(videoId) {
|
|
13827
|
+
if (!mediaResolverConfigured()) return null;
|
|
13828
|
+
return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "audio-whisper" }, async () => {
|
|
13829
|
+
const resolved = await resolveYouTubeAudio(videoId);
|
|
13830
|
+
if (!resolved) return null;
|
|
13831
|
+
try {
|
|
13832
|
+
const transcript = await transcribeMediaUrl(resolved.mediaUrl, `# Transcript \u2014 ${videoId}`);
|
|
13833
|
+
if (!transcript.text) return null;
|
|
13834
|
+
const chunks = transcript.chunks.map((c) => ({ timestamp: c.timestamp, text: c.text.trim() }));
|
|
13835
|
+
return {
|
|
13836
|
+
videoId,
|
|
13837
|
+
text: transcript.text,
|
|
13838
|
+
chunks,
|
|
13839
|
+
durationMs: resolved.durationS > 0 ? Math.round(resolved.durationS * 1e3) : transcript.durationMs,
|
|
13840
|
+
method: "audio-whisper"
|
|
13841
|
+
};
|
|
13842
|
+
} catch (err) {
|
|
13843
|
+
console.warn(`[CaptionFetcher] transcription of resolved audio failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13844
|
+
return null;
|
|
13845
|
+
}
|
|
13846
|
+
});
|
|
13818
13847
|
}
|
|
13819
13848
|
function finalizeCaptions(result) {
|
|
13820
13849
|
const chunks = result.chunks.map((c) => {
|
|
@@ -13836,18 +13865,21 @@ async function fetchCaptions(videoId) {
|
|
|
13836
13865
|
if (primary) return finalizeCaptions(primary);
|
|
13837
13866
|
const result = await fetchViaKernelSession(videoId);
|
|
13838
13867
|
if (result) return finalizeCaptions(result);
|
|
13868
|
+
const viaResolvedAudio = await fetchViaResolvedAudio(videoId);
|
|
13869
|
+
if (viaResolvedAudio) return finalizeCaptions(viaResolvedAudio);
|
|
13839
13870
|
throw new Error(
|
|
13840
|
-
`Could not transcribe ${videoId}
|
|
13871
|
+
`Could not transcribe ${videoId}: it has no caption track and its audio could not be retrieved.`
|
|
13841
13872
|
);
|
|
13842
13873
|
}
|
|
13843
|
-
var
|
|
13874
|
+
var INNERTUBE_CLIENTS, YT_RESIDENTIAL_LOCATION;
|
|
13844
13875
|
var init_CaptionFetcher = __esm({
|
|
13845
13876
|
"src/youtube/CaptionFetcher.ts"() {
|
|
13846
13877
|
"use strict";
|
|
13847
13878
|
init_BrowserDriver();
|
|
13848
13879
|
init_browser_service_env();
|
|
13849
13880
|
init_kernel_proxy_resolver();
|
|
13850
|
-
|
|
13881
|
+
init_audio_resolver();
|
|
13882
|
+
init_media_transcription();
|
|
13851
13883
|
init_cost_telemetry();
|
|
13852
13884
|
init_cost_context();
|
|
13853
13885
|
INNERTUBE_CLIENTS = [
|
|
@@ -13855,7 +13887,6 @@ var init_CaptionFetcher = __esm({
|
|
|
13855
13887
|
{ name: "IOS", version: "20.10.4", userAgent: "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)" },
|
|
13856
13888
|
{ name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
|
|
13857
13889
|
];
|
|
13858
|
-
MAX_AUDIO_BYTES = 96 * 1024 * 1024;
|
|
13859
13890
|
YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
|
|
13860
13891
|
}
|
|
13861
13892
|
});
|
|
@@ -15392,46 +15423,6 @@ var init_schemas3 = __esm({
|
|
|
15392
15423
|
}
|
|
15393
15424
|
});
|
|
15394
15425
|
|
|
15395
|
-
// src/services/media-transcription.ts
|
|
15396
|
-
function transcriptMarkdown(title, text, chunks, durationMs) {
|
|
15397
|
-
const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
|
15398
|
-
const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
|
|
15399
|
-
if (chunks.length) {
|
|
15400
|
-
lines.push("## Timestamped Segments", "");
|
|
15401
|
-
for (const ch of chunks) {
|
|
15402
|
-
lines.push(`**[${fmtTs2(ch.timestamp[0])} -> ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
|
|
15403
|
-
}
|
|
15404
|
-
}
|
|
15405
|
-
return lines.join("\n");
|
|
15406
|
-
}
|
|
15407
|
-
async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
|
|
15408
|
-
const startMs = Date.now();
|
|
15409
|
-
const result = await import_client4.fal.subscribe("fal-ai/wizper", {
|
|
15410
|
-
input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
|
|
15411
|
-
logs: false,
|
|
15412
|
-
pollInterval: 3e3
|
|
15413
|
-
});
|
|
15414
|
-
const data = result.data;
|
|
15415
|
-
const text = data.text ?? "";
|
|
15416
|
-
const chunks = data.chunks ?? [];
|
|
15417
|
-
const durationMs = Date.now() - startMs;
|
|
15418
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
|
|
15419
|
-
return {
|
|
15420
|
-
text,
|
|
15421
|
-
chunks,
|
|
15422
|
-
durationMs,
|
|
15423
|
-
markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
|
|
15424
|
-
};
|
|
15425
|
-
}
|
|
15426
|
-
var import_client4;
|
|
15427
|
-
var init_media_transcription = __esm({
|
|
15428
|
-
"src/services/media-transcription.ts"() {
|
|
15429
|
-
"use strict";
|
|
15430
|
-
import_client4 = require("@fal-ai/client");
|
|
15431
|
-
init_cost_telemetry();
|
|
15432
|
-
}
|
|
15433
|
-
});
|
|
15434
|
-
|
|
15435
15426
|
// src/api/facebook-ad-routes.ts
|
|
15436
15427
|
function invalidRequest(message) {
|
|
15437
15428
|
return { error_code: "invalid_request", message };
|
|
@@ -15474,7 +15465,7 @@ async function kernelLaunchOptsResidential() {
|
|
|
15474
15465
|
}
|
|
15475
15466
|
return { headless: true, kernelApiKey: browserServiceApiKey(), kernelProxyId: proxyId, viewport: { width: 1280, height: 900 }, locale: "en-US" };
|
|
15476
15467
|
}
|
|
15477
|
-
var import_hono5, import_zod16,
|
|
15468
|
+
var import_hono5, import_zod16, import_client4, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, facebookAdApp, ALLOWED_MEDIA_HOSTS;
|
|
15478
15469
|
var init_facebook_ad_routes = __esm({
|
|
15479
15470
|
"src/api/facebook-ad-routes.ts"() {
|
|
15480
15471
|
"use strict";
|
|
@@ -15483,13 +15474,14 @@ var init_facebook_ad_routes = __esm({
|
|
|
15483
15474
|
import_zod16 = require("zod");
|
|
15484
15475
|
init_db();
|
|
15485
15476
|
init_rates();
|
|
15477
|
+
init_cost_rates();
|
|
15486
15478
|
init_BrowserDriver();
|
|
15487
15479
|
init_FacebookAdExtractor();
|
|
15488
15480
|
init_FacebookAdGraphql();
|
|
15489
15481
|
init_FacebookOrganicVideoExtractor();
|
|
15490
15482
|
init_kernel_proxy_resolver();
|
|
15491
15483
|
init_schemas3();
|
|
15492
|
-
|
|
15484
|
+
import_client4 = require("@fal-ai/client");
|
|
15493
15485
|
init_api_auth();
|
|
15494
15486
|
init_url_utils();
|
|
15495
15487
|
init_concurrency_gates();
|
|
@@ -15632,17 +15624,17 @@ var init_facebook_ad_routes = __esm({
|
|
|
15632
15624
|
metadata: { videoUrl }
|
|
15633
15625
|
});
|
|
15634
15626
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
15635
|
-
|
|
15636
|
-
const holdMc =
|
|
15627
|
+
import_client4.fal.config({ credentials: process.env.FAL_KEY });
|
|
15628
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
15637
15629
|
let debited = false;
|
|
15638
15630
|
try {
|
|
15639
15631
|
const { ok, balance_mc } = await debitMc(fbUser.id, holdMc, LedgerOperation.FB_TRANSCRIBE, videoUrl);
|
|
15640
15632
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
|
|
15641
15633
|
debited = true;
|
|
15642
15634
|
const transcript = await transcribeFacebookVideoUrl(videoUrl, "# Facebook Ad Transcript");
|
|
15643
|
-
const
|
|
15644
|
-
const diff = holdMc -
|
|
15645
|
-
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "
|
|
15635
|
+
const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
|
|
15636
|
+
const diff = holdMc - settleMc;
|
|
15637
|
+
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
15646
15638
|
else if (diff < 0) await debitMc(fbUser.id, -diff, LedgerOperation.FB_TRANSCRIBE, videoUrl);
|
|
15647
15639
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_transcribe", status: "done", query: videoUrl, resultCount: transcript.chunks.length, result: { text: transcript.text, chunks: transcript.chunks, durationMs: transcript.durationMs } });
|
|
15648
15640
|
return c.json(transcript);
|
|
@@ -15676,22 +15668,24 @@ var init_facebook_ad_routes = __esm({
|
|
|
15676
15668
|
metadata: { url: sourceUrl.href }
|
|
15677
15669
|
});
|
|
15678
15670
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
15679
|
-
|
|
15671
|
+
import_client4.fal.config({ credentials: process.env.FAL_KEY });
|
|
15680
15672
|
const driver = new BrowserDriver();
|
|
15681
|
-
const holdMc =
|
|
15673
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
15682
15674
|
let debited = false;
|
|
15683
15675
|
try {
|
|
15684
15676
|
const { ok, balance_mc } = await debitMc(fbUser.id, holdMc, LedgerOperation.FB_TRANSCRIBE, sourceUrl.href);
|
|
15685
15677
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
|
|
15686
15678
|
debited = true;
|
|
15679
|
+
const kernelStartMs = Date.now();
|
|
15687
15680
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
15688
15681
|
await driver.navigateTo(sourceUrl.href);
|
|
15689
15682
|
const page = driver.getPage();
|
|
15690
15683
|
const video = await extractFacebookOrganicVideoFromPage(page, sourceUrl.href, body.quality);
|
|
15684
|
+
const kernelMs = Date.now() - kernelStartMs;
|
|
15691
15685
|
const transcript = await transcribeFacebookVideoUrl(video.videoUrl, "# Facebook Organic Video Transcript");
|
|
15692
|
-
const
|
|
15693
|
-
const diff = holdMc -
|
|
15694
|
-
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "
|
|
15686
|
+
const settleMc = Math.round((transcript.costUsd + kernelCostUsd(kernelMs, true)) * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
|
|
15687
|
+
const diff = holdMc - settleMc;
|
|
15688
|
+
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
15695
15689
|
else if (diff < 0) await debitMc(fbUser.id, -diff, LedgerOperation.FB_TRANSCRIBE, sourceUrl.href);
|
|
15696
15690
|
const result = {
|
|
15697
15691
|
...video,
|
|
@@ -16055,7 +16049,7 @@ async function kernelLaunchOptsResidential2() {
|
|
|
16055
16049
|
function isBlockedMessage(msg) {
|
|
16056
16050
|
return msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha");
|
|
16057
16051
|
}
|
|
16058
|
-
var import_hono6, import_zod17,
|
|
16052
|
+
var import_hono6, import_zod17, import_client5, GoogleAdsSearchBodySchema, GoogleAdsPageIntelBodySchema, GoogleAdsTranscribeBodySchema, googleAdsApp;
|
|
16059
16053
|
var init_google_ads_routes = __esm({
|
|
16060
16054
|
"src/api/google-ads-routes.ts"() {
|
|
16061
16055
|
"use strict";
|
|
@@ -16068,11 +16062,12 @@ var init_google_ads_routes = __esm({
|
|
|
16068
16062
|
init_GoogleAdsExtractor();
|
|
16069
16063
|
init_kernel_proxy_resolver();
|
|
16070
16064
|
init_schemas3();
|
|
16071
|
-
|
|
16065
|
+
import_client5 = require("@fal-ai/client");
|
|
16072
16066
|
init_api_auth();
|
|
16073
16067
|
init_url_utils();
|
|
16074
16068
|
init_concurrency_gates();
|
|
16075
16069
|
init_media_transcription();
|
|
16070
|
+
init_cost_context();
|
|
16076
16071
|
GoogleAdsSearchBodySchema = import_zod17.z.object({
|
|
16077
16072
|
query: import_zod17.z.string().trim().min(1, "query is required"),
|
|
16078
16073
|
region: import_zod17.z.string().trim().toUpperCase().length(2).optional(),
|
|
@@ -16104,9 +16099,11 @@ var init_google_ads_routes = __esm({
|
|
|
16104
16099
|
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH, body.query.trim());
|
|
16105
16100
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.google_ads_search), 402);
|
|
16106
16101
|
debited = true;
|
|
16107
|
-
await
|
|
16108
|
-
|
|
16109
|
-
|
|
16102
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "google_ads_search", userId: user.id }, async () => {
|
|
16103
|
+
await driver.launch(await kernelLaunchOptsResidential2());
|
|
16104
|
+
const extractor = new GoogleAdsExtractor(driver);
|
|
16105
|
+
return extractor.search(body.query.trim(), region, maxResults);
|
|
16106
|
+
});
|
|
16110
16107
|
if (result.advertisers.length === 0) {
|
|
16111
16108
|
await creditMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH_REFUND, "empty result");
|
|
16112
16109
|
refunded = true;
|
|
@@ -16142,9 +16139,11 @@ var init_google_ads_routes = __esm({
|
|
|
16142
16139
|
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL, queryLabel);
|
|
16143
16140
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.google_ads_intel), 402);
|
|
16144
16141
|
debited = true;
|
|
16145
|
-
await
|
|
16146
|
-
|
|
16147
|
-
|
|
16142
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "google_ads_intel", userId: user.id }, async () => {
|
|
16143
|
+
await driver.launch(await kernelLaunchOptsResidential2());
|
|
16144
|
+
const extractor = new GoogleAdsExtractor(driver);
|
|
16145
|
+
return extractor.pageIntel({ advertiserId: body.advertiserId, domain: body.domain, region, maxAds });
|
|
16146
|
+
});
|
|
16148
16147
|
if (result.ads.length === 0) {
|
|
16149
16148
|
await creditMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL_REFUND, "empty result");
|
|
16150
16149
|
refunded = true;
|
|
@@ -16174,17 +16173,17 @@ var init_google_ads_routes = __esm({
|
|
|
16174
16173
|
const user = c.get("user");
|
|
16175
16174
|
const gate = await acquireConcurrencyGate(user, "google_ads_transcribe", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { videoUrl } });
|
|
16176
16175
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
16177
|
-
|
|
16178
|
-
const holdMc =
|
|
16176
|
+
import_client5.fal.config({ credentials: process.env.FAL_KEY });
|
|
16177
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
16179
16178
|
let debited = false;
|
|
16180
16179
|
try {
|
|
16181
16180
|
const { ok, balance_mc } = await debitMc(user.id, holdMc, LedgerOperation.GOOGLE_ADS_TRANSCRIBE, videoUrl);
|
|
16182
16181
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
|
|
16183
16182
|
debited = true;
|
|
16184
|
-
const transcript = await transcribeMediaUrl(videoUrl, "# Google Ad Transcript");
|
|
16185
|
-
const
|
|
16186
|
-
const diff = holdMc -
|
|
16187
|
-
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE_REFUND, "
|
|
16183
|
+
const transcript = await runWithCostContext({ ...currentCostContext(), op: "google_ads_transcribe", userId: user.id }, () => transcribeMediaUrl(videoUrl, "# Google Ad Transcript"));
|
|
16184
|
+
const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
|
|
16185
|
+
const diff = holdMc - settleMc;
|
|
16186
|
+
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
16188
16187
|
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE, videoUrl);
|
|
16189
16188
|
await logRequestEvent({ userId: user.id, source: "google_ads_transcribe", status: "done", query: videoUrl, resultCount: transcript.chunks.length, result: { text: transcript.text, chunks: transcript.chunks, durationMs: transcript.durationMs } });
|
|
16190
16189
|
return c.json(transcript);
|
|
@@ -16852,10 +16851,15 @@ var init_instagram_routes = __esm({
|
|
|
16852
16851
|
if (!audioUrl) {
|
|
16853
16852
|
warnings.push("Transcript requested, but no audio/video track was available.");
|
|
16854
16853
|
} else {
|
|
16855
|
-
const
|
|
16856
|
-
|
|
16854
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
16855
|
+
const tx = await debitMc(user.id, holdMc, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
|
|
16856
|
+
if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, holdMc), 402);
|
|
16857
16857
|
transcriptDebited = true;
|
|
16858
16858
|
transcript = await transcribeMediaUrl(audioUrl, "# Instagram Media Transcript");
|
|
16859
|
+
const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
|
|
16860
|
+
const diff = holdMc - settleMc;
|
|
16861
|
+
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
16862
|
+
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
|
|
16859
16863
|
}
|
|
16860
16864
|
}
|
|
16861
16865
|
const result = {
|
|
@@ -16870,7 +16874,7 @@ var init_instagram_routes = __esm({
|
|
|
16870
16874
|
return c.json(result);
|
|
16871
16875
|
} catch (err) {
|
|
16872
16876
|
const msg = err instanceof Error ? err.message : String(err);
|
|
16873
|
-
if (transcriptDebited) await creditMc(user.id,
|
|
16877
|
+
if (transcriptDebited) await creditMc(user.id, MEDIA_TRANSCRIBE_HOLD_MC, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "failed call");
|
|
16874
16878
|
if (mediaDebited) await creditMc(user.id, MC_COSTS.instagram_media, LedgerOperation.INSTAGRAM_MEDIA_REFUND, "failed call");
|
|
16875
16879
|
await logRequestEvent({ userId: user.id, source: "instagram_media_download", status: "failed", query: sourceUrl.href, error: msg });
|
|
16876
16880
|
if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) return c.json({ error: msg }, 503);
|
|
@@ -17011,10 +17015,23 @@ var init_reddit_routes = __esm({
|
|
|
17011
17015
|
result.comments = result.comments.slice(0, body.maxComments);
|
|
17012
17016
|
result.numComments = result.comments.length;
|
|
17013
17017
|
}
|
|
17018
|
+
const commentCount = Array.isArray(result.comments) ? result.comments.length : 0;
|
|
17019
|
+
commentDebitMc = commentCount * MC_COSTS.reddit_comment;
|
|
17020
|
+
if (commentDebitMc > 0) {
|
|
17021
|
+
const commentDebit = await debitMc(user.id, commentDebitMc, LedgerOperation.REDDIT_COMMENT, body.url);
|
|
17022
|
+
if (!commentDebit.ok) {
|
|
17023
|
+
const affordable = Math.floor((commentDebit.balance_mc ?? 0) / MC_COSTS.reddit_comment);
|
|
17024
|
+
result.comments = result.comments.slice(0, affordable);
|
|
17025
|
+
result.numComments = affordable;
|
|
17026
|
+
commentDebitMc = affordable * MC_COSTS.reddit_comment;
|
|
17027
|
+
if (commentDebitMc > 0) await debitMc(user.id, commentDebitMc, LedgerOperation.REDDIT_COMMENT, body.url);
|
|
17028
|
+
}
|
|
17029
|
+
}
|
|
17014
17030
|
await logRequestEvent({ userId: user.id, source: "reddit_thread", status: "done", query: body.url, resultCount: Number(result.numComments ?? 0), result });
|
|
17015
17031
|
return c.json(result);
|
|
17016
17032
|
} catch (err) {
|
|
17017
17033
|
if (debited && !refunded) await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "failed call");
|
|
17034
|
+
if (commentDebitMc > 0) await creditMc(user.id, commentDebitMc, LedgerOperation.REDDIT_COMMENT_REFUND, "failed call");
|
|
17018
17035
|
const msg = err instanceof Error ? err.message : String(err);
|
|
17019
17036
|
await logRequestEvent({ userId: user.id, source: "reddit_thread", status: "failed", query: body.url, error: msg });
|
|
17020
17037
|
return c.json({ error: msg }, 500);
|
|
@@ -27287,7 +27304,7 @@ var PACKAGE_VERSION;
|
|
|
27287
27304
|
var init_version = __esm({
|
|
27288
27305
|
"src/version.ts"() {
|
|
27289
27306
|
"use strict";
|
|
27290
|
-
PACKAGE_VERSION = "0.4.
|
|
27307
|
+
PACKAGE_VERSION = "0.4.14";
|
|
27291
27308
|
}
|
|
27292
27309
|
});
|
|
27293
27310
|
|
|
@@ -38373,6 +38390,11 @@ function opForCostPath(p) {
|
|
|
38373
38390
|
return "fb_ad";
|
|
38374
38391
|
}
|
|
38375
38392
|
if (p.startsWith("/instagram/")) return "instagram";
|
|
38393
|
+
if (p.startsWith("/google-ads/")) {
|
|
38394
|
+
if (p.endsWith("/transcribe")) return "google_ads_transcribe";
|
|
38395
|
+
if (p.endsWith("/search")) return "google_ads_search";
|
|
38396
|
+
return "google_ads_intel";
|
|
38397
|
+
}
|
|
38376
38398
|
if (p === "/reddit/thread") return "reddit_thread";
|
|
38377
38399
|
if (p.startsWith("/trustpilot/")) return "trustpilot_reviews";
|
|
38378
38400
|
if (p.startsWith("/g2/")) return "g2_reviews";
|