tuna-agent 0.1.154 → 0.1.156
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.
|
@@ -116,6 +116,65 @@ function run(cmd, args, opts = {}) {
|
|
|
116
116
|
p.on('close', (code) => code === 0 ? resolve({ out, err }) : reject(new Error(`${cmd} exit ${code}: ${err.slice(0, 500)}`)));
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
+
// Modern Chrome UA — TikTok/Douyin reject unknown user-agents.
|
|
120
|
+
const SRC_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
121
|
+
// Optional cookies for auth-gated sources (Facebook private/page videos,
|
|
122
|
+
// some Douyin). YT_DLP_COOKIES = path to a Netscape cookies.txt; or
|
|
123
|
+
// YT_DLP_COOKIES_FROM_BROWSER = a browser name yt-dlp can read cookies from.
|
|
124
|
+
function cookieArgs() {
|
|
125
|
+
if (process.env.YT_DLP_COOKIES)
|
|
126
|
+
return ['--cookies', process.env.YT_DLP_COOKIES];
|
|
127
|
+
if (process.env.YT_DLP_COOKIES_FROM_BROWSER)
|
|
128
|
+
return ['--cookies-from-browser', process.env.YT_DLP_COOKIES_FROM_BROWSER];
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
function detectPlatform(url) {
|
|
132
|
+
let h = '';
|
|
133
|
+
try {
|
|
134
|
+
h = new URL(url).hostname.toLowerCase();
|
|
135
|
+
}
|
|
136
|
+
catch { /* malformed → 'other' */ }
|
|
137
|
+
if (/(^|\.)youtube\.com$|(^|\.)youtu\.be$/.test(h))
|
|
138
|
+
return 'youtube';
|
|
139
|
+
if (/(^|\.)tiktok\.com$/.test(h))
|
|
140
|
+
return 'tiktok';
|
|
141
|
+
if (/douyin\.com$|iesdouyin\.com$/.test(h))
|
|
142
|
+
return 'douyin';
|
|
143
|
+
if (/facebook\.com$|fb\.watch$|(^|\.)fb\.com$/.test(h))
|
|
144
|
+
return 'facebook';
|
|
145
|
+
return 'other';
|
|
146
|
+
}
|
|
147
|
+
// Download a source video across YouTube / TikTok / Douyin / Facebook.
|
|
148
|
+
// yt-dlp supports all of them, but a single rigid `-f` that works for
|
|
149
|
+
// YouTube fails on the others, so try a tolerant 720p-capped format then
|
|
150
|
+
// fall back to letting yt-dlp pick. UA + optional cookies harden the
|
|
151
|
+
// non-YouTube extractors (FB private/page + Douyin need cookies).
|
|
152
|
+
async function downloadSourceVideo(url, dest) {
|
|
153
|
+
const platform = detectPlatform(url);
|
|
154
|
+
const common = [
|
|
155
|
+
'--no-playlist', '--no-warnings', '--retries', '3', '--fragment-retries', '3',
|
|
156
|
+
'--user-agent', SRC_UA, ...cookieArgs(), '-o', dest, url,
|
|
157
|
+
];
|
|
158
|
+
const attempts = [
|
|
159
|
+
['-f', 'bv*[height<=720]+ba/b[height<=720]/best', ...common],
|
|
160
|
+
['-f', 'best/mp4', ...common], // let yt-dlp choose (TikTok/Douyin/FB quirks)
|
|
161
|
+
];
|
|
162
|
+
let lastErr;
|
|
163
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
164
|
+
try {
|
|
165
|
+
await run(YT_DLP, attempts[i]);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
lastErr = e;
|
|
170
|
+
console.warn(`[analyze_video] yt-dlp attempt ${i + 1}/${attempts.length} failed (${platform}): ${String(e?.message || e).slice(0, 220)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const hint = (platform === 'facebook' || platform === 'douyin')
|
|
174
|
+
? ' — FB private/page & some Douyin need cookies (set YT_DLP_COOKIES)'
|
|
175
|
+
: '';
|
|
176
|
+
throw new Error(`yt-dlp failed for ${platform} after ${attempts.length} attempts${hint}: ${String(lastErr?.message || lastErr).slice(0, 300)}`);
|
|
177
|
+
}
|
|
119
178
|
async function whisperTranscribe(audioPath) {
|
|
120
179
|
if (!OPENAI_KEY)
|
|
121
180
|
throw new Error('OPENAI_API_KEY not set');
|
|
@@ -450,7 +509,7 @@ export async function analyzeVideo(url, onProgress) {
|
|
|
450
509
|
// analyze of the same URL never reads a half-written file.
|
|
451
510
|
const dlTmp = path.join(CACHE_DIR, `${urlHash}.dl-${crypto.randomBytes(4).toString('hex')}.mp4`);
|
|
452
511
|
try {
|
|
453
|
-
await
|
|
512
|
+
await downloadSourceVideo(url, dlTmp);
|
|
454
513
|
await fs.rename(dlTmp, videoPath);
|
|
455
514
|
}
|
|
456
515
|
catch (e) {
|
|
@@ -462,7 +521,7 @@ export async function analyzeVideo(url, onProgress) {
|
|
|
462
521
|
// clone idea gets a real name instead of "Clone: www.youtube.com".
|
|
463
522
|
let source_title = '';
|
|
464
523
|
try {
|
|
465
|
-
const t = await run(YT_DLP, ['--skip-download', '--no-warnings', '--no-playlist', '--print', '%(title)s', url]);
|
|
524
|
+
const t = await run(YT_DLP, ['--skip-download', '--no-warnings', '--no-playlist', '--user-agent', SRC_UA, ...cookieArgs(), '--print', '%(title)s', url]);
|
|
466
525
|
source_title = (t.out || '').trim().split('\n')[0].slice(0, 200);
|
|
467
526
|
}
|
|
468
527
|
catch { /* title is best-effort — analysis still proceeds without it */ }
|
|
@@ -118,21 +118,26 @@ export async function handleClaudePrompt(ws, code, taskId, prompt, systemPrompt,
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
// ─── Handler: claude_prompt_stream (prompt → streamed text + JSON result) ────
|
|
121
|
-
// Track active stream tasks by
|
|
121
|
+
// Track active stream tasks by taskId (UNIQUE per request). Was keyed by
|
|
122
|
+
// `code` (the shared pair code) which made concurrent claude_prompt_stream
|
|
123
|
+
// calls abort each other — fatal for the batched clone pool (5 parallel
|
|
124
|
+
// chunks → siblings killed → batches hang at 0). taskId is unique so streams
|
|
125
|
+
// run independently; a superseded request gets a fresh taskId anyway.
|
|
122
126
|
const _activeStreamTasks = new Map();
|
|
123
127
|
export async function handleClaudePromptStream(ws, code, taskId, prompt, systemPrompt) {
|
|
124
128
|
console.log(`[claude_prompt_stream] Received: ${prompt.substring(0, 100)}...`);
|
|
125
|
-
//
|
|
126
|
-
|
|
129
|
+
// Per-taskId (unique): never aborts a concurrent sibling. (Re-clicked
|
|
130
|
+
// Regenerate arrives with a new taskId; the old one finishes on its own.)
|
|
131
|
+
const prev = _activeStreamTasks.get(taskId);
|
|
127
132
|
if (prev) {
|
|
128
|
-
console.log(`[claude_prompt_stream] Aborting previous task for
|
|
133
|
+
console.log(`[claude_prompt_stream] Aborting previous task for taskId=${taskId}`);
|
|
129
134
|
prev.abort();
|
|
130
135
|
}
|
|
131
136
|
const abortController = new AbortController();
|
|
132
|
-
_activeStreamTasks.set(
|
|
137
|
+
_activeStreamTasks.set(taskId, abortController);
|
|
133
138
|
if (!hasContentCreator()) {
|
|
134
139
|
const error = 'content-creator agent not found on this machine';
|
|
135
|
-
_activeStreamTasks.delete(
|
|
140
|
+
_activeStreamTasks.delete(taskId);
|
|
136
141
|
ws.sendExtensionDone(code, taskId, { error });
|
|
137
142
|
return;
|
|
138
143
|
}
|
|
@@ -190,11 +195,11 @@ export async function handleClaudePromptStream(ws, code, taskId, prompt, systemP
|
|
|
190
195
|
}
|
|
191
196
|
catch { }
|
|
192
197
|
}
|
|
193
|
-
_activeStreamTasks.delete(
|
|
198
|
+
_activeStreamTasks.delete(taskId);
|
|
194
199
|
ws.sendExtensionDone(code, taskId, { result: parsed, raw: text });
|
|
195
200
|
}
|
|
196
201
|
catch (err) {
|
|
197
|
-
_activeStreamTasks.delete(
|
|
202
|
+
_activeStreamTasks.delete(taskId);
|
|
198
203
|
if (abortController.signal.aborted) {
|
|
199
204
|
console.log(`[claude_prompt_stream] Aborted (replaced by new task)`);
|
|
200
205
|
return; // Don't send error — new task is already running
|