mcp-scraper 0.4.6 → 0.4.12
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 +1532 -1351
- 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.cjs +13 -4
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/{chunk-5GAD2AN2.js → chunk-2YY46QYT.js} +27 -10
- package/dist/chunk-2YY46QYT.js.map +1 -0
- package/dist/{chunk-IFXACD4K.js → chunk-6EXP6DQG.js} +2 -2
- package/dist/{chunk-IVQYNY45.js → chunk-EMY7ELRU.js} +23 -6
- package/dist/chunk-EMY7ELRU.js.map +1 -0
- package/dist/{chunk-NYAWN7CZ.js → chunk-ONIOF5XW.js} +2 -2
- package/dist/chunk-SHXJQQOH.js +7 -0
- package/dist/chunk-SHXJQQOH.js.map +1 -0
- package/dist/index.cjs +59 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/{server-R3KVQNOT.js → server-UNID3SJU.js} +409 -272
- package/dist/server-UNID3SJU.js.map +1 -0
- package/dist/{site-extract-repository-LI2S3S6E.js → site-extract-repository-HTIY52MW.js} +3 -3
- package/dist/{worker-VASAJ7FZ.js → worker-HTYZAYGB.js} +16 -12
- package/dist/worker-HTYZAYGB.js.map +1 -0
- package/package.json +1 -2
- package/dist/chunk-5BB6Y3BB.js +0 -7
- package/dist/chunk-5BB6Y3BB.js.map +0 -1
- package/dist/chunk-5GAD2AN2.js.map +0 -1
- package/dist/chunk-IVQYNY45.js.map +0 -1
- package/dist/server-R3KVQNOT.js.map +0 -1
- package/dist/worker-VASAJ7FZ.js.map +0 -1
- /package/dist/{chunk-IFXACD4K.js.map → chunk-6EXP6DQG.js.map} +0 -0
- /package/dist/{chunk-NYAWN7CZ.js.map → chunk-ONIOF5XW.js.map} +0 -0
- /package/dist/{site-extract-repository-LI2S3S6E.js.map → site-extract-repository-HTIY52MW.js.map} +0 -0
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
recordVendorUsage,
|
|
17
17
|
resolveKernelProxyId,
|
|
18
18
|
runWithCostContext
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-2YY46QYT.js";
|
|
20
20
|
import {
|
|
21
21
|
HttpMcpToolExecutor,
|
|
22
22
|
MemoryMcpToolExecutor,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
registerSerpIntelligenceCaptureTools,
|
|
32
32
|
sanitizeAttempts,
|
|
33
33
|
sanitizeHarvestResult
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-6EXP6DQG.js";
|
|
35
35
|
import {
|
|
36
36
|
auditImages,
|
|
37
37
|
getBlobStore
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
RawMapsOverviewSchema,
|
|
66
66
|
RawMapsReviewStatsSchema
|
|
67
67
|
} from "./chunk-XGIPATLV.js";
|
|
68
|
-
import "./chunk-
|
|
68
|
+
import "./chunk-SHXJQQOH.js";
|
|
69
69
|
import {
|
|
70
70
|
completeExtractJob,
|
|
71
71
|
countSuccessfulPages,
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
saveExtractPages,
|
|
77
77
|
setExtractJobTotal,
|
|
78
78
|
settleExtractJob
|
|
79
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-ONIOF5XW.js";
|
|
80
80
|
import {
|
|
81
81
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
82
82
|
CONCURRENCY_PRICE_ID,
|
|
@@ -90,12 +90,14 @@ import {
|
|
|
90
90
|
MEMORY_PLANS,
|
|
91
91
|
MEMORY_PLAN_QUOTA,
|
|
92
92
|
SCHEDULING_PLANS,
|
|
93
|
+
SITE_AUDIT_LLM_MARGIN_MULTIPLE,
|
|
93
94
|
SUBSCRIPTION_TIERS,
|
|
94
95
|
SUBSCRIPTION_TIER_BY_KEY,
|
|
96
|
+
VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE,
|
|
95
97
|
browserActiveCostMc,
|
|
96
98
|
concurrencySlotBillingInfo,
|
|
97
99
|
insufficientBalanceResponse
|
|
98
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-EMY7ELRU.js";
|
|
99
101
|
import {
|
|
100
102
|
CaptchaError,
|
|
101
103
|
RECAPTCHA_INSTRUCTIONS,
|
|
@@ -5339,6 +5341,7 @@ function extractJson(raw) {
|
|
|
5339
5341
|
var DeepInfraLlmClient = class {
|
|
5340
5342
|
apiKey;
|
|
5341
5343
|
model;
|
|
5344
|
+
totalCostUsd = 0;
|
|
5342
5345
|
constructor(apiKey, model = "Qwen/Qwen3.6-35B-A3B") {
|
|
5343
5346
|
this.apiKey = apiKey;
|
|
5344
5347
|
this.model = model;
|
|
@@ -5371,6 +5374,7 @@ ${raw}`
|
|
|
5371
5374
|
var OpenRouterLlmClient = class {
|
|
5372
5375
|
apiKey;
|
|
5373
5376
|
model;
|
|
5377
|
+
totalCostUsd = 0;
|
|
5374
5378
|
constructor(apiKey, model = "qwen/qwen3-235b-a22b") {
|
|
5375
5379
|
this.apiKey = apiKey;
|
|
5376
5380
|
this.model = model;
|
|
@@ -5383,6 +5387,9 @@ var OpenRouterLlmClient = class {
|
|
|
5383
5387
|
});
|
|
5384
5388
|
if (!resp.ok) throw new Error(`OpenRouter error ${resp.status}: ${await resp.text()}`);
|
|
5385
5389
|
const data = await resp.json();
|
|
5390
|
+
const cost = data.usage?.cost ?? 0;
|
|
5391
|
+
this.totalCostUsd += cost;
|
|
5392
|
+
void recordVendorUsage({ vendor: "openrouter", model: this.model, units: cost, unitType: "usd" });
|
|
5386
5393
|
return data.choices[0].message.content;
|
|
5387
5394
|
}
|
|
5388
5395
|
async completeJson(prompt) {
|
|
@@ -5408,6 +5415,9 @@ var FallbackLlmClient = class {
|
|
|
5408
5415
|
secondary;
|
|
5409
5416
|
lastServedBy = null;
|
|
5410
5417
|
lastPrimaryError = null;
|
|
5418
|
+
get totalCostUsd() {
|
|
5419
|
+
return this.primary.totalCostUsd + this.secondary.totalCostUsd;
|
|
5420
|
+
}
|
|
5411
5421
|
async complete(prompt) {
|
|
5412
5422
|
try {
|
|
5413
5423
|
const result = await this.primary.complete(prompt);
|
|
@@ -6894,8 +6904,23 @@ var SiteAuditService = class {
|
|
|
6894
6904
|
async getJobsForUser(userId) {
|
|
6895
6905
|
return this.deps.repo.listSiteAuditJobs(userId);
|
|
6896
6906
|
}
|
|
6907
|
+
async runLlmBilled(userId, source, fn) {
|
|
6908
|
+
const before = this.deps.llm.totalCostUsd;
|
|
6909
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "audit_site", userId }, fn);
|
|
6910
|
+
const deltaCostUsd = this.deps.llm.totalCostUsd - before;
|
|
6911
|
+
if (deltaCostUsd <= 0) return result;
|
|
6912
|
+
const mc = Math.round(deltaCostUsd * SITE_AUDIT_LLM_MARGIN_MULTIPLE * MC_PER_USD);
|
|
6913
|
+
if (mc <= 0) return result;
|
|
6914
|
+
const { ok } = await debitMc(userId, mc, LedgerOperation.SITE_AUDIT_LLM, source);
|
|
6915
|
+
if (!ok) {
|
|
6916
|
+
console.warn(`[site-audit-service] SITE_AUDIT_LLM surcharge debit failed for user ${userId} (${source}): insufficient balance for ${mc}mc`);
|
|
6917
|
+
}
|
|
6918
|
+
return result;
|
|
6919
|
+
}
|
|
6897
6920
|
async runIngestPhase(jobId, payload) {
|
|
6898
6921
|
const { request } = payload;
|
|
6922
|
+
const job = await this.deps.repo.getSiteAuditJob(jobId);
|
|
6923
|
+
if (!job) throw new Error(`[site-audit-service] runIngestPhase: job ${jobId} not found`);
|
|
6899
6924
|
const input = {
|
|
6900
6925
|
clientDomain: request.clientDomain,
|
|
6901
6926
|
sfInternalPath: request.sfInternalPath,
|
|
@@ -6910,14 +6935,18 @@ var SiteAuditService = class {
|
|
|
6910
6935
|
businessContext: request.businessContext,
|
|
6911
6936
|
priorAuditMemoryEnabled: request.priorAuditMemoryEnabled
|
|
6912
6937
|
};
|
|
6913
|
-
const result = await
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6938
|
+
const result = await this.runLlmBilled(
|
|
6939
|
+
job.user_id,
|
|
6940
|
+
`${jobId}:phase1-ingest`,
|
|
6941
|
+
() => llmParseWithRetry(
|
|
6942
|
+
ValidatedFileManifestSchema,
|
|
6943
|
+
(attempt, error) => {
|
|
6944
|
+
void attempt;
|
|
6945
|
+
void error;
|
|
6946
|
+
return buildIngestValidatePrompt(input);
|
|
6947
|
+
},
|
|
6948
|
+
this.deps.llm
|
|
6949
|
+
)
|
|
6921
6950
|
);
|
|
6922
6951
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase1-ingest", result);
|
|
6923
6952
|
}
|
|
@@ -6932,23 +6961,27 @@ var SiteAuditService = class {
|
|
|
6932
6961
|
const competitorDomainsCsvPath = request.competitorDomainsCsvPath;
|
|
6933
6962
|
void competitorDomainsCsvPath;
|
|
6934
6963
|
const competitors = await this._readCompetitorDomains(request);
|
|
6935
|
-
const results = await
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
(
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6964
|
+
const results = await this.runLlmBilled(
|
|
6965
|
+
job.user_id,
|
|
6966
|
+
`${jobId}:phase1-enrich`,
|
|
6967
|
+
() => Promise.all(
|
|
6968
|
+
competitors.map(
|
|
6969
|
+
(domain, idx) => limit(async () => {
|
|
6970
|
+
const competitorIndex = idx + 1;
|
|
6971
|
+
const rawPrompt = buildEnrichCompetitorPrompt({ domain, sessionPath });
|
|
6972
|
+
const prompt = rawPrompt.replace("<competitor-index>", String(competitorIndex));
|
|
6973
|
+
const result = await llmParseWithRetry(
|
|
6974
|
+
EnrichCompetitorOutputSchema,
|
|
6975
|
+
(attempt, error) => {
|
|
6976
|
+
void attempt;
|
|
6977
|
+
void error;
|
|
6978
|
+
return prompt;
|
|
6979
|
+
},
|
|
6980
|
+
this.deps.llm
|
|
6981
|
+
);
|
|
6982
|
+
return result;
|
|
6983
|
+
})
|
|
6984
|
+
)
|
|
6952
6985
|
)
|
|
6953
6986
|
);
|
|
6954
6987
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase1-ingest", { competitors: results });
|
|
@@ -6962,19 +6995,23 @@ var SiteAuditService = class {
|
|
|
6962
6995
|
const clientDomain = request.clientDomain;
|
|
6963
6996
|
const urlInventory = [];
|
|
6964
6997
|
const directedEdgeCount = 0;
|
|
6965
|
-
const orphanAnnotation = await
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6998
|
+
const orphanAnnotation = await this.runLlmBilled(
|
|
6999
|
+
job.user_id,
|
|
7000
|
+
`${jobId}:phase2-build-graph`,
|
|
7001
|
+
() => llmParseWithRetry(
|
|
7002
|
+
OrphanAnnotationOutputSchema,
|
|
7003
|
+
(attempt, error) => {
|
|
7004
|
+
void attempt;
|
|
7005
|
+
void error;
|
|
7006
|
+
return buildGraphPerSitePrompt({
|
|
7007
|
+
siteId: "client",
|
|
7008
|
+
domain: clientDomain,
|
|
7009
|
+
urlInventory,
|
|
7010
|
+
directedEdgeCount
|
|
7011
|
+
});
|
|
7012
|
+
},
|
|
7013
|
+
this.deps.llm
|
|
7014
|
+
)
|
|
6978
7015
|
);
|
|
6979
7016
|
const graphMetrics = await this.deps.python.runGraphMetrics({
|
|
6980
7017
|
directedLinkGraphPath: `${sessionPath}/phase1/directed_link_graph.json`,
|
|
@@ -7019,50 +7056,52 @@ var SiteAuditService = class {
|
|
|
7019
7056
|
}
|
|
7020
7057
|
if (currentBatch.length > 0) batches.push(currentBatch);
|
|
7021
7058
|
const usZipsCsvPath = process.env["US_ZIPS_CSV_PATH"] ?? "/Users/vilovieta/Downloads/sales-magician-api-leads-magician-01c6cff78e31/tools/analytics/data/uszips.csv";
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7059
|
+
await this.runLlmBilled(job.user_id, `${jobId}:phase3-classify`, async () => {
|
|
7060
|
+
for (const batch of batches) {
|
|
7061
|
+
let classified = [];
|
|
7062
|
+
let unclassifiedUrls = batch.map((u) => u.url);
|
|
7063
|
+
try {
|
|
7064
|
+
const pythonResult = await this.deps.python.runLocationClassifier({
|
|
7065
|
+
urls: batch.map((u) => u.url),
|
|
7066
|
+
usZipsCsvPath
|
|
7067
|
+
});
|
|
7068
|
+
classified = pythonResult.classified;
|
|
7069
|
+
unclassifiedUrls = pythonResult.unclassified;
|
|
7070
|
+
} catch (err) {
|
|
7071
|
+
console.warn(
|
|
7072
|
+
"[site-audit-service] runClassifyPhase: python.runLocationClassifier failed (falling back to LLM):",
|
|
7073
|
+
err instanceof Error ? err.message : String(err)
|
|
7074
|
+
);
|
|
7075
|
+
unclassifiedUrls = batch.map((u) => u.url);
|
|
7076
|
+
}
|
|
7077
|
+
for (const c of classified) {
|
|
7078
|
+
allClassifications.push({
|
|
7079
|
+
url: c.url,
|
|
7080
|
+
page_type: c.pageType,
|
|
7081
|
+
confidence: c.confidence > 0.6 ? "high" : c.confidence > 0.4 ? "medium" : "low",
|
|
7082
|
+
_classification_label: "[COMPUTED:python/location-classifier]",
|
|
7083
|
+
topic_cluster: null,
|
|
7084
|
+
location_signals: c.locationSignals
|
|
7085
|
+
});
|
|
7086
|
+
}
|
|
7087
|
+
if (unclassifiedUrls.length > 0) {
|
|
7088
|
+
const unclassifiedBatch = batch.filter((u) => unclassifiedUrls.includes(u.url));
|
|
7089
|
+
const llmResults = await llmParseWithRetry(
|
|
7090
|
+
z9.array(PageTypeClassificationRowSchema),
|
|
7091
|
+
(attempt, error) => {
|
|
7092
|
+
void attempt;
|
|
7093
|
+
void error;
|
|
7094
|
+
return buildMetricsClassifyPrompt({
|
|
7095
|
+
urls: unclassifiedBatch,
|
|
7096
|
+
siteDomain: clientDomain
|
|
7097
|
+
});
|
|
7098
|
+
},
|
|
7099
|
+
this.deps.llm
|
|
7100
|
+
);
|
|
7101
|
+
allClassifications.push(...llmResults);
|
|
7102
|
+
}
|
|
7064
7103
|
}
|
|
7065
|
-
}
|
|
7104
|
+
});
|
|
7066
7105
|
void sessionPath;
|
|
7067
7106
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase3-classify", {
|
|
7068
7107
|
classifications: allClassifications,
|
|
@@ -7075,20 +7114,24 @@ var SiteAuditService = class {
|
|
|
7075
7114
|
if (!job) throw new Error(`[site-audit-service] runComparePhase: job ${jobId} not found`);
|
|
7076
7115
|
const request = JSON.parse(job.request);
|
|
7077
7116
|
const clientDomain = request.clientDomain;
|
|
7078
|
-
const result = await
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7117
|
+
const result = await this.runLlmBilled(
|
|
7118
|
+
job.user_id,
|
|
7119
|
+
`${jobId}:phase4-compare`,
|
|
7120
|
+
() => llmParseWithRetry(
|
|
7121
|
+
CompareRecommendOutputSchema,
|
|
7122
|
+
(attempt, error) => {
|
|
7123
|
+
void attempt;
|
|
7124
|
+
void error;
|
|
7125
|
+
return buildCompareRecommendPrompt({
|
|
7126
|
+
clientDomain,
|
|
7127
|
+
clientMoneyPages: [],
|
|
7128
|
+
competitorSites: [],
|
|
7129
|
+
clientUrlMetrics: [],
|
|
7130
|
+
clientPageTypes: []
|
|
7131
|
+
});
|
|
7132
|
+
},
|
|
7133
|
+
this.deps.llm
|
|
7134
|
+
)
|
|
7092
7135
|
);
|
|
7093
7136
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase4-compare", result);
|
|
7094
7137
|
return result;
|
|
@@ -7118,20 +7161,24 @@ var SiteAuditService = class {
|
|
|
7118
7161
|
total: ilesTotal,
|
|
7119
7162
|
components: ilesComponents
|
|
7120
7163
|
};
|
|
7121
|
-
const reportMarkdown = await
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7164
|
+
const reportMarkdown = await this.runLlmBilled(
|
|
7165
|
+
job.user_id,
|
|
7166
|
+
`${jobId}:phase5-synthesize`,
|
|
7167
|
+
() => llmParseWithRetry(
|
|
7168
|
+
z9.string(),
|
|
7169
|
+
(attempt, error) => {
|
|
7170
|
+
void attempt;
|
|
7171
|
+
void error;
|
|
7172
|
+
return buildScoreSynthesizePrompt({
|
|
7173
|
+
preComputedArchitectureScore,
|
|
7174
|
+
preComputedIlesScore,
|
|
7175
|
+
rankedBacklog: [],
|
|
7176
|
+
clientDomain,
|
|
7177
|
+
sessionPath
|
|
7178
|
+
});
|
|
7179
|
+
},
|
|
7180
|
+
this.deps.llm
|
|
7181
|
+
)
|
|
7135
7182
|
);
|
|
7136
7183
|
const auditResult = SiteAuditResultSchema.parse({
|
|
7137
7184
|
jobId,
|
|
@@ -8140,6 +8187,11 @@ internalMemoryApp.post("/ai-debit", async (c) => {
|
|
|
8140
8187
|
}
|
|
8141
8188
|
const user = await getUserByEmail(body.identity.trim().toLowerCase());
|
|
8142
8189
|
if (!user) return c.json({ ok: false, reason: "no_user" }, 200);
|
|
8190
|
+
if (body.costUsd > 0) {
|
|
8191
|
+
await runWithCostContext({ ...currentCostContext(), op: "memory_ai", userId: user.id }, async () => {
|
|
8192
|
+
void recordVendorUsage({ vendor: "mcp_memory_ai", model: null, units: body.costUsd, unitType: "usd", error: null });
|
|
8193
|
+
});
|
|
8194
|
+
}
|
|
8143
8195
|
const mc = Math.round(body.costUsd * MEMORY_AI_MARGIN_MULTIPLE * MC_PER_USD);
|
|
8144
8196
|
if (mc <= 0) return c.json({ ok: true, mc: 0, balance_mc: null }, 200);
|
|
8145
8197
|
const { ok, balance_mc } = await debitMc(user.id, mc, LedgerOperation.MEMORY_AI, `${body.source ?? "ai"}:${body.op ?? ""}`);
|
|
@@ -8660,6 +8712,11 @@ async function fetchViaYoutubeTranscript(videoId) {
|
|
|
8660
8712
|
}));
|
|
8661
8713
|
const text = chunks.map((c) => c.text).join(" ");
|
|
8662
8714
|
const last = chunks[chunks.length - 1];
|
|
8715
|
+
const now = Date.now();
|
|
8716
|
+
await runWithCostContext(
|
|
8717
|
+
{ ...currentCostContext(), op: "yt_transcription", subOp: "youtube-transcript" },
|
|
8718
|
+
() => recordKernelSession({ kernelSessionId: null, source: "yt_caption_tier", headlessSent: null, openedAtMs: now, closedAtMs: now })
|
|
8719
|
+
);
|
|
8663
8720
|
return { videoId, text, chunks, durationMs: last ? last.timestamp[1] * 1e3 : 0, method: "youtube-transcript" };
|
|
8664
8721
|
} catch {
|
|
8665
8722
|
return null;
|
|
@@ -8711,163 +8768,221 @@ function parseTimedtextXml(xml) {
|
|
|
8711
8768
|
}
|
|
8712
8769
|
return results;
|
|
8713
8770
|
}
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8771
|
+
var INNERTUBE_CLIENTS = [
|
|
8772
|
+
{ name: "ANDROID", version: "20.10.38", userAgent: "com.google.android.youtube/20.10.38 (Linux; U; Android 14)" },
|
|
8773
|
+
{ 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;)" },
|
|
8774
|
+
{ name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
|
|
8775
|
+
];
|
|
8776
|
+
function isBotBlockStatus(status) {
|
|
8777
|
+
return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
|
|
8778
|
+
}
|
|
8779
|
+
async function probePlayer(page, videoId) {
|
|
8780
|
+
return page.evaluate(
|
|
8781
|
+
async ({ vid, clients }) => {
|
|
8782
|
+
let lastStatus = null;
|
|
8783
|
+
for (const client2 of clients) {
|
|
8784
|
+
const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
|
|
8785
|
+
method: "POST",
|
|
8786
|
+
headers: { "Content-Type": "application/json", "User-Agent": client2.userAgent },
|
|
8787
|
+
body: JSON.stringify({
|
|
8788
|
+
context: { client: { clientName: client2.name, clientVersion: client2.version } },
|
|
8789
|
+
videoId: vid
|
|
8790
|
+
})
|
|
8791
|
+
}).catch(() => null);
|
|
8792
|
+
if (!resp?.ok) {
|
|
8793
|
+
lastStatus = resp ? `player_http_${resp.status}` : "player_fetch_failed";
|
|
8794
|
+
continue;
|
|
8795
|
+
}
|
|
8796
|
+
const data = await resp.json();
|
|
8797
|
+
lastStatus = data?.playabilityStatus?.status ?? null;
|
|
8798
|
+
const audioFormats = (data?.streamingData?.adaptiveFormats ?? []).filter((f) => typeof f.url === "string" && f.url && typeof f.mimeType === "string" && f.mimeType.startsWith("audio/")).sort((a, b) => (Number(a.contentLength) || Infinity) - (Number(b.contentLength) || Infinity));
|
|
8799
|
+
const pick = audioFormats[0] ?? null;
|
|
8800
|
+
let captions = null;
|
|
8801
|
+
const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
8802
|
+
if (tracks.length > 0) {
|
|
8803
|
+
const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
|
|
8804
|
+
const sep = track.baseUrl.includes("?") ? "&" : "?";
|
|
8805
|
+
const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
|
|
8806
|
+
if (xmlResp?.ok) captions = await xmlResp.text();
|
|
8807
|
+
}
|
|
8808
|
+
if (!captions && !pick) continue;
|
|
8809
|
+
return {
|
|
8810
|
+
captions,
|
|
8811
|
+
status: lastStatus,
|
|
8812
|
+
audio: pick ? {
|
|
8813
|
+
url: pick.url,
|
|
8814
|
+
mimeType: pick.mimeType,
|
|
8815
|
+
bitrate: pick.bitrate ?? null,
|
|
8816
|
+
contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
|
|
8817
|
+
approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
|
|
8818
|
+
} : null
|
|
8819
|
+
};
|
|
8820
|
+
}
|
|
8821
|
+
return { captions: null, audio: null, status: lastStatus };
|
|
8822
|
+
},
|
|
8823
|
+
{ vid: videoId, clients: INNERTUBE_CLIENTS }
|
|
8824
|
+
).catch(() => null);
|
|
8825
|
+
}
|
|
8826
|
+
var MAX_AUDIO_BYTES = 96 * 1024 * 1024;
|
|
8827
|
+
async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
|
|
8828
|
+
if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
|
|
8829
|
+
console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
|
|
8830
|
+
return null;
|
|
8831
|
+
}
|
|
8832
|
+
const sep = audio.url.includes("?") ? "&" : "?";
|
|
8833
|
+
const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
|
|
8834
|
+
const result = await page.evaluate(async (u) => {
|
|
8835
|
+
try {
|
|
8836
|
+
const resp = await fetch(u);
|
|
8837
|
+
if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
|
|
8838
|
+
const blob = await resp.blob();
|
|
8839
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
8840
|
+
const reader = new FileReader();
|
|
8841
|
+
reader.onload = () => resolve(reader.result);
|
|
8842
|
+
reader.onerror = () => reject(reader.error);
|
|
8843
|
+
reader.readAsDataURL(blob);
|
|
8740
8844
|
});
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
return xmlResp.ok ? xmlResp.text() : null;
|
|
8749
|
-
}, videoId);
|
|
8750
|
-
if (!xml) return null;
|
|
8751
|
-
let entries = parseTimedtextJson3(xml);
|
|
8752
|
-
if (entries.length === 0) entries = parseTimedtextXml(xml);
|
|
8753
|
-
if (entries.length === 0) return null;
|
|
8754
|
-
const chunks = entries.map((e) => ({
|
|
8755
|
-
timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
|
|
8756
|
-
text: e.text
|
|
8757
|
-
}));
|
|
8758
|
-
const text = chunks.map((c) => c.text).join(" ");
|
|
8759
|
-
const last = chunks[chunks.length - 1];
|
|
8760
|
-
return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
|
|
8761
|
-
} catch {
|
|
8845
|
+
return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
|
|
8846
|
+
} catch (e) {
|
|
8847
|
+
return { ok: false, error: String(e) };
|
|
8848
|
+
}
|
|
8849
|
+
}, url).catch((e) => ({ ok: false, error: String(e) }));
|
|
8850
|
+
if (!result.ok || !result.base64) {
|
|
8851
|
+
console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
|
|
8762
8852
|
return null;
|
|
8763
|
-
} finally {
|
|
8764
|
-
await driver.close();
|
|
8765
8853
|
}
|
|
8854
|
+
const bytes = Buffer.from(result.base64, "base64");
|
|
8855
|
+
if (audio.contentLength && bytes.length < audio.contentLength) {
|
|
8856
|
+
console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
|
|
8857
|
+
return null;
|
|
8858
|
+
}
|
|
8859
|
+
return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
|
|
8766
8860
|
}
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
return new Promise((resolve) => {
|
|
8771
|
-
const video = document.querySelector("video");
|
|
8772
|
-
if (!video) {
|
|
8773
|
-
resolve({ ok: false, error: "no video element" });
|
|
8774
|
-
return;
|
|
8775
|
-
}
|
|
8776
|
-
video.muted = false;
|
|
8777
|
-
video.volume = 1;
|
|
8778
|
-
try {
|
|
8779
|
-
const ctx = new AudioContext();
|
|
8780
|
-
const src = ctx.createMediaElementSource(video);
|
|
8781
|
-
const dest = ctx.createMediaStreamDestination();
|
|
8782
|
-
src.connect(dest);
|
|
8783
|
-
src.connect(ctx.destination);
|
|
8784
|
-
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/ogg";
|
|
8785
|
-
const recorder = new MediaRecorder(dest.stream, { mimeType, audioBitsPerSecond: 48e3 });
|
|
8786
|
-
const chunks = [];
|
|
8787
|
-
recorder.ondataavailable = (e) => {
|
|
8788
|
-
if (e.data.size > 0) chunks.push(e.data);
|
|
8789
|
-
};
|
|
8790
|
-
recorder.onstop = async () => {
|
|
8791
|
-
const blob = new Blob(chunks, { type: mimeType });
|
|
8792
|
-
const ab = await blob.arrayBuffer();
|
|
8793
|
-
const arr = new Uint8Array(ab);
|
|
8794
|
-
let binary = "";
|
|
8795
|
-
const chunkSize = 8192;
|
|
8796
|
-
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
8797
|
-
binary += String.fromCharCode(...arr.slice(i, Math.min(i + chunkSize, arr.length)));
|
|
8798
|
-
}
|
|
8799
|
-
resolve({ ok: true, base64: btoa(binary), size: ab.byteLength, mimeType });
|
|
8800
|
-
};
|
|
8801
|
-
recorder.start(1e3);
|
|
8802
|
-
video.playbackRate = 2;
|
|
8803
|
-
video.play().catch(() => void 0);
|
|
8804
|
-
setTimeout(() => {
|
|
8805
|
-
recorder.stop();
|
|
8806
|
-
ctx.close();
|
|
8807
|
-
}, recordSecs * 1e3);
|
|
8808
|
-
} catch (e) {
|
|
8809
|
-
resolve({ ok: false, error: String(e) });
|
|
8810
|
-
}
|
|
8811
|
-
});
|
|
8812
|
-
}, WHISPER_RECORD_SECONDS);
|
|
8813
|
-
if (!result.ok || !result.base64 || !result.mimeType) return null;
|
|
8814
|
-
return { bytes: Buffer.from(result.base64, "base64"), mimeType: result.mimeType };
|
|
8815
|
-
}
|
|
8816
|
-
async function attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey, start) {
|
|
8817
|
-
const driver = new BrowserDriver();
|
|
8861
|
+
async function transcribeWithWizper(videoId, audio, startMs) {
|
|
8862
|
+
const falKey = process.env.FAL_KEY;
|
|
8863
|
+
if (!falKey) return null;
|
|
8818
8864
|
try {
|
|
8819
|
-
await driver.launch({ kernelApiKey, kernelProxyId, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
|
|
8820
|
-
const page = driver.getPage();
|
|
8821
|
-
await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
|
|
8822
|
-
await page.waitForFunction(
|
|
8823
|
-
() => !!window.ytcfg,
|
|
8824
|
-
{ timeout: 2e4 }
|
|
8825
|
-
);
|
|
8826
|
-
const playStatus = await page.evaluate(() => {
|
|
8827
|
-
const w = window;
|
|
8828
|
-
return w.ytInitialPlayerResponse?.playabilityStatus?.status;
|
|
8829
|
-
});
|
|
8830
|
-
if (playStatus !== "OK") {
|
|
8831
|
-
await driver.close();
|
|
8832
|
-
return null;
|
|
8833
|
-
}
|
|
8834
|
-
const audioInfo = await captureAudioViaMediaRecorder(page);
|
|
8835
|
-
await driver.close();
|
|
8836
|
-
if (!audioInfo || audioInfo.bytes.length < 1e4) return null;
|
|
8837
8865
|
fal.config({ credentials: falKey });
|
|
8838
|
-
const ext =
|
|
8839
|
-
const audioFile = new File([new Uint8Array(
|
|
8866
|
+
const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
|
|
8867
|
+
const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
|
|
8840
8868
|
const uploadedUrl = await fal.storage.upload(audioFile);
|
|
8869
|
+
const subscribeStartMs = Date.now();
|
|
8841
8870
|
const result = await fal.subscribe("fal-ai/wizper", {
|
|
8842
|
-
input: { audio_url: uploadedUrl, task: "transcribe", language: "en" },
|
|
8871
|
+
input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
|
|
8843
8872
|
logs: false,
|
|
8844
8873
|
pollInterval: 3e3
|
|
8845
8874
|
});
|
|
8875
|
+
const subscribeDurationMs = Date.now() - subscribeStartMs;
|
|
8846
8876
|
const data = result.data;
|
|
8847
8877
|
const chunks = (data.chunks ?? []).map((c) => ({
|
|
8848
8878
|
timestamp: c.timestamp,
|
|
8849
8879
|
text: c.text.trim()
|
|
8850
8880
|
}));
|
|
8851
|
-
|
|
8852
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: audioSec / 60, unitType: "audio_min" });
|
|
8881
|
+
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
|
|
8853
8882
|
const text = data.text ?? chunks.map((c) => c.text).join(" ");
|
|
8854
8883
|
if (!text) return null;
|
|
8855
|
-
return { videoId, text, chunks, durationMs: Date.now() -
|
|
8856
|
-
} catch {
|
|
8857
|
-
|
|
8884
|
+
return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
|
|
8885
|
+
} catch (err) {
|
|
8886
|
+
console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8858
8887
|
return null;
|
|
8859
8888
|
}
|
|
8860
8889
|
}
|
|
8861
|
-
|
|
8890
|
+
var YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
|
|
8891
|
+
async function resolveResidentialProxy(kernelApiKey) {
|
|
8892
|
+
try {
|
|
8893
|
+
const resolved = await resolveKernelProxyId({
|
|
8894
|
+
kernelApiKey,
|
|
8895
|
+
proxyMode: "location",
|
|
8896
|
+
location: YT_RESIDENTIAL_LOCATION,
|
|
8897
|
+
gl: "us",
|
|
8898
|
+
fresh: true
|
|
8899
|
+
});
|
|
8900
|
+
return { kernelProxyId: resolved.kernelProxyId, disposableProxyId: resolved.disposableProxyId };
|
|
8901
|
+
} catch (err) {
|
|
8902
|
+
console.warn(`[CaptionFetcher] residential proxy resolution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8903
|
+
return {};
|
|
8904
|
+
}
|
|
8905
|
+
}
|
|
8906
|
+
async function probeAndExtract(videoId, opts) {
|
|
8862
8907
|
const kernelApiKey = browserServiceApiKey();
|
|
8863
|
-
|
|
8864
|
-
const
|
|
8865
|
-
|
|
8908
|
+
if (!kernelApiKey) return null;
|
|
8909
|
+
const kernelProxyId = opts?.kernelProxyId ?? browserServiceProxyId();
|
|
8910
|
+
const driver = new BrowserDriver();
|
|
8911
|
+
return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
|
|
8912
|
+
let entries = [];
|
|
8913
|
+
let audio = null;
|
|
8914
|
+
let retryWithFreshIp = false;
|
|
8915
|
+
try {
|
|
8916
|
+
await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
|
|
8917
|
+
const page = driver.getPage();
|
|
8918
|
+
await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
|
|
8919
|
+
await page.waitForFunction(
|
|
8920
|
+
() => !!window.ytcfg,
|
|
8921
|
+
{ timeout: 2e4 }
|
|
8922
|
+
);
|
|
8923
|
+
const probe = await probePlayer(page, videoId);
|
|
8924
|
+
if (!probe) return null;
|
|
8925
|
+
if (probe.captions && !opts?.skipCaptions) {
|
|
8926
|
+
entries = parseTimedtextJson3(probe.captions);
|
|
8927
|
+
if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
|
|
8928
|
+
}
|
|
8929
|
+
if (entries.length === 0) {
|
|
8930
|
+
if (!probe.audio) {
|
|
8931
|
+
retryWithFreshIp = isBotBlockStatus(probe.status);
|
|
8932
|
+
console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
|
|
8933
|
+
return { entries, audio: null, retryWithFreshIp };
|
|
8934
|
+
}
|
|
8935
|
+
audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
|
|
8936
|
+
if (!audio) return { entries, audio: null, retryWithFreshIp: true };
|
|
8937
|
+
}
|
|
8938
|
+
} catch (err) {
|
|
8939
|
+
console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8940
|
+
return null;
|
|
8941
|
+
} finally {
|
|
8942
|
+
const servedSubOp = audio ? "browser-whisper" : "browser-innertube";
|
|
8943
|
+
await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
|
|
8944
|
+
}
|
|
8945
|
+
return { entries, audio, retryWithFreshIp };
|
|
8946
|
+
});
|
|
8947
|
+
}
|
|
8948
|
+
async function extractWithProxyEscalation(videoId, opts) {
|
|
8949
|
+
const direct = await probeAndExtract(videoId, opts);
|
|
8950
|
+
if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
|
|
8951
|
+
const shouldEscalate = direct === null || direct.retryWithFreshIp;
|
|
8952
|
+
if (!shouldEscalate) return direct;
|
|
8953
|
+
const kernelApiKey = browserServiceApiKey();
|
|
8954
|
+
if (!kernelApiKey) return direct;
|
|
8955
|
+
const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
|
|
8956
|
+
if (!kernelProxyId) return direct;
|
|
8957
|
+
try {
|
|
8958
|
+
console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
|
|
8959
|
+
const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
|
|
8960
|
+
return viaProxy ?? direct;
|
|
8961
|
+
} finally {
|
|
8962
|
+
if (disposableProxyId) {
|
|
8963
|
+
await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
|
|
8964
|
+
(err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
|
|
8965
|
+
);
|
|
8966
|
+
}
|
|
8967
|
+
}
|
|
8968
|
+
}
|
|
8969
|
+
async function fetchViaKernelSession(videoId) {
|
|
8866
8970
|
const start = Date.now();
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8971
|
+
const outcome = await extractWithProxyEscalation(videoId);
|
|
8972
|
+
if (!outcome) return null;
|
|
8973
|
+
if (outcome.entries.length > 0) {
|
|
8974
|
+
const chunks = outcome.entries.map((e) => ({
|
|
8975
|
+
timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
|
|
8976
|
+
text: e.text
|
|
8977
|
+
}));
|
|
8978
|
+
const text = chunks.map((c) => c.text).join(" ");
|
|
8979
|
+
return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
|
|
8980
|
+
}
|
|
8981
|
+
if (outcome.audio) {
|
|
8982
|
+
return runWithCostContext(
|
|
8983
|
+
{ ...currentCostContext(), op: "yt_transcription", subOp: "browser-whisper" },
|
|
8984
|
+
() => transcribeWithWizper(videoId, outcome.audio, start)
|
|
8985
|
+
);
|
|
8871
8986
|
}
|
|
8872
8987
|
return null;
|
|
8873
8988
|
}
|
|
@@ -8889,11 +9004,11 @@ function finalizeCaptions(result) {
|
|
|
8889
9004
|
async function fetchCaptions(videoId) {
|
|
8890
9005
|
const primary = await fetchViaYoutubeTranscript(videoId);
|
|
8891
9006
|
if (primary) return finalizeCaptions(primary);
|
|
8892
|
-
const
|
|
8893
|
-
if (
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
9007
|
+
const result = await fetchViaKernelSession(videoId);
|
|
9008
|
+
if (result) return finalizeCaptions(result);
|
|
9009
|
+
throw new Error(
|
|
9010
|
+
`Could not transcribe ${videoId}. It has no caption track, and its audio stream could not be downloaded \u2014 YouTube permits a single media request per IP, so streams that do not fit in one response are unreachable. Captioned videos of any length still work.`
|
|
9011
|
+
);
|
|
8897
9012
|
}
|
|
8898
9013
|
|
|
8899
9014
|
// src/api/api-auth.ts
|
|
@@ -9221,7 +9336,7 @@ youtubeApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
9221
9336
|
const { ok: trOk, balance_mc: trBal } = await debitMc(user.id, holdMc, LedgerOperation.TRANSCRIPTION_HOLD, id);
|
|
9222
9337
|
if (!trOk) return c.json(insufficientBalanceResponse(trBal, holdMc), 402);
|
|
9223
9338
|
debited = true;
|
|
9224
|
-
const result = await fetchCaptions(id);
|
|
9339
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", userId: user.id }, () => fetchCaptions(id));
|
|
9225
9340
|
const lastChunk = result.chunks?.[result.chunks.length - 1];
|
|
9226
9341
|
const chunkSecs = lastChunk && Number.isFinite(lastChunk.timestamp[1]) ? lastChunk.timestamp[1] : 0;
|
|
9227
9342
|
const durationSecs = Number.isFinite(result.durationMs) ? result.durationMs / 1e3 : 0;
|
|
@@ -10284,8 +10399,7 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
|
|
|
10284
10399
|
const text = data.text ?? "";
|
|
10285
10400
|
const chunks = data.chunks ?? [];
|
|
10286
10401
|
const durationMs = Date.now() - startMs;
|
|
10287
|
-
|
|
10288
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: audioSec / 60, unitType: "audio_min" });
|
|
10402
|
+
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
|
|
10289
10403
|
return {
|
|
10290
10404
|
text,
|
|
10291
10405
|
chunks,
|
|
@@ -11760,6 +11874,7 @@ redditApp.post("/thread", createApiKeyAuth(), async (c) => {
|
|
|
11760
11874
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
11761
11875
|
let debited = false;
|
|
11762
11876
|
let refunded = false;
|
|
11877
|
+
let commentDebitMc = 0;
|
|
11763
11878
|
try {
|
|
11764
11879
|
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, body.url);
|
|
11765
11880
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.reddit_thread), 402);
|
|
@@ -12077,6 +12192,21 @@ videoApp.post("/status", createApiKeyAuth(), async (c) => {
|
|
|
12077
12192
|
effectiveFrames: st.frameCount ?? null
|
|
12078
12193
|
};
|
|
12079
12194
|
}
|
|
12195
|
+
if ((st.status === "done" || st.status === "failed") && typeof st.costUsd === "number" && st.costUsd > 0) {
|
|
12196
|
+
const llmOp = `${LedgerOperation.VIDEO_ANALYSIS_LLM}:${runId}`;
|
|
12197
|
+
if (!await ledgerExistsForOperation(user.id, llmOp)) {
|
|
12198
|
+
await runWithCostContext({ ...currentCostContext(), op: "video_analysis", userId: user.id }, async () => {
|
|
12199
|
+
void recordVendorUsage({ vendor: "mcp_memory_video", model: null, units: st.costUsd, unitType: "usd", error: null });
|
|
12200
|
+
});
|
|
12201
|
+
const surchargeMc = Math.round(st.costUsd * VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE * MC_PER_USD);
|
|
12202
|
+
if (surchargeMc > 0) {
|
|
12203
|
+
const { ok } = await debitMc(user.id, surchargeMc, llmOp, `real-cost-surcharge:${runId}`);
|
|
12204
|
+
if (!ok) {
|
|
12205
|
+
console.warn(`[video-routes] VIDEO_ANALYSIS_LLM surcharge debit failed for user ${user.id} (run ${runId}): insufficient balance for ${surchargeMc}mc`);
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
}
|
|
12080
12210
|
return c.json({
|
|
12081
12211
|
ok: true,
|
|
12082
12212
|
runId,
|
|
@@ -13247,7 +13377,7 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
|
|
|
13247
13377
|
let pagesFetched = 0;
|
|
13248
13378
|
try {
|
|
13249
13379
|
await driver.launch({
|
|
13250
|
-
headless:
|
|
13380
|
+
headless: false,
|
|
13251
13381
|
kernelApiKey: browserServiceApiKey(),
|
|
13252
13382
|
viewport: { width: 1280, height: 900 },
|
|
13253
13383
|
locale: "en-US"
|
|
@@ -13440,7 +13570,7 @@ g2App.post("/reviews", createApiKeyAuth(), async (c) => {
|
|
|
13440
13570
|
const driver = new BrowserDriver();
|
|
13441
13571
|
try {
|
|
13442
13572
|
await driver.launch({
|
|
13443
|
-
headless:
|
|
13573
|
+
headless: false,
|
|
13444
13574
|
kernelApiKey: browserServiceApiKey(),
|
|
13445
13575
|
kernelProxyId: proxyId,
|
|
13446
13576
|
viewport: { width: 1280, height: 900 },
|
|
@@ -20279,9 +20409,12 @@ function opForCostPath(p) {
|
|
|
20279
20409
|
}
|
|
20280
20410
|
if (p.startsWith("/instagram/")) return "instagram";
|
|
20281
20411
|
if (p === "/reddit/thread") return "reddit_thread";
|
|
20412
|
+
if (p.startsWith("/trustpilot/")) return "trustpilot_reviews";
|
|
20413
|
+
if (p.startsWith("/g2/")) return "g2_reviews";
|
|
20282
20414
|
if (p.startsWith("/api/internal/site-architecture-auditor")) return "audit_site";
|
|
20283
20415
|
if (p.startsWith("/api/internal/memory")) return "memory_ai";
|
|
20284
20416
|
if (p.startsWith("/agent/")) return "browser_session";
|
|
20417
|
+
if (p.startsWith("/video/")) return "video_analysis";
|
|
20285
20418
|
return null;
|
|
20286
20419
|
}
|
|
20287
20420
|
app.use("*", (c, next) => {
|
|
@@ -20993,16 +21126,20 @@ app.post("/harvest/sync", auth2, async (c) => {
|
|
|
20993
21126
|
AbortSignal.timeout(syncTimeoutMs)
|
|
20994
21127
|
]);
|
|
20995
21128
|
try {
|
|
20996
|
-
const
|
|
20997
|
-
|
|
20998
|
-
|
|
20999
|
-
|
|
21000
|
-
|
|
21001
|
-
|
|
21002
|
-
|
|
21003
|
-
|
|
21004
|
-
|
|
21005
|
-
|
|
21129
|
+
const harvestCtx = currentCostContext();
|
|
21130
|
+
const result = await runWithCostContext(
|
|
21131
|
+
{ ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
|
|
21132
|
+
() => harvest({
|
|
21133
|
+
...options,
|
|
21134
|
+
kernelApiKey: browserServiceApiKey(),
|
|
21135
|
+
headless: true,
|
|
21136
|
+
format: "json",
|
|
21137
|
+
outputDir: "/tmp/paa-output-api",
|
|
21138
|
+
signal: syncSignal,
|
|
21139
|
+
softDeadlineMs: Date.now() + Math.floor(syncTimeoutMs * 0.8),
|
|
21140
|
+
onAttemptEvent: recordAttempt
|
|
21141
|
+
})
|
|
21142
|
+
);
|
|
21006
21143
|
await completeJob(jobId, result);
|
|
21007
21144
|
const attempts = await listHarvestAttempts(jobId, user.id);
|
|
21008
21145
|
if (!options.serpOnly) {
|
|
@@ -21681,7 +21818,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
21681
21818
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
21682
21819
|
return c.json({ error: "Unauthorized" }, 401);
|
|
21683
21820
|
}
|
|
21684
|
-
const { drainQueue } = await import("./worker-
|
|
21821
|
+
const { drainQueue } = await import("./worker-HTYZAYGB.js");
|
|
21685
21822
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
21686
21823
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
21687
21824
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
|
|
@@ -21699,7 +21836,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
21699
21836
|
return c.json({ error: "Unauthorized" }, 401);
|
|
21700
21837
|
}
|
|
21701
21838
|
const jobId = c.req.param("id");
|
|
21702
|
-
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-
|
|
21839
|
+
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-HTIY52MW.js");
|
|
21703
21840
|
const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
|
|
21704
21841
|
const job = await getExtractJob2(jobId);
|
|
21705
21842
|
if (!job) return c.json({ error: "job not found" }, 404);
|
|
@@ -21707,7 +21844,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
21707
21844
|
await completeExtractJob2(jobId, stored);
|
|
21708
21845
|
let settlement = "already_settled_or_refunded";
|
|
21709
21846
|
if (job.billedMc == null && job.userId != null) {
|
|
21710
|
-
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
21847
|
+
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-HTIY52MW.js");
|
|
21711
21848
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
21712
21849
|
const successful = await countSuccessfulPages2(jobId);
|
|
21713
21850
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -21871,4 +22008,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
21871
22008
|
export {
|
|
21872
22009
|
app
|
|
21873
22010
|
};
|
|
21874
|
-
//# sourceMappingURL=server-
|
|
22011
|
+
//# sourceMappingURL=server-UNID3SJU.js.map
|