tuna-agent 0.1.156 → 0.1.158
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.
|
@@ -144,6 +144,22 @@ function detectPlatform(url) {
|
|
|
144
144
|
return 'facebook';
|
|
145
145
|
return 'other';
|
|
146
146
|
}
|
|
147
|
+
// yt-dlp `%(title)s` is a clean title on YouTube but the full post caption
|
|
148
|
+
// on TikTok/Douyin/Facebook (often 200+ chars, prefixed with engagement
|
|
149
|
+
// stats). Strip the social-stats prefix, keep the first segment, and cap
|
|
150
|
+
// to a sane title length so the cloned idea doesn't get a paragraph title.
|
|
151
|
+
function cleanSourceTitle(raw) {
|
|
152
|
+
let t = (raw || '').trim().split('\n')[0].trim();
|
|
153
|
+
// Drop leading "1K views · 51 reactions | " / "230 likes, 12 comments - " noise.
|
|
154
|
+
t = t.replace(/^\s*(?:[\d.,]+\s*[KMB]?\s*(?:views?|likes?|reactions?|comments?|shares?|followers?)\b\s*[·,|–\-:]*\s*)+/i, '').trim();
|
|
155
|
+
// FB/TikTok pack "title | extra | hashtags" — keep the first real segment.
|
|
156
|
+
const seg = t.split(/\s*[|·]\s*/)[0].trim();
|
|
157
|
+
if (seg.length >= 8)
|
|
158
|
+
t = seg;
|
|
159
|
+
if (t.length > 90)
|
|
160
|
+
t = t.slice(0, 90).replace(/\s+\S*$/, '').trim() + '…';
|
|
161
|
+
return t.slice(0, 120);
|
|
162
|
+
}
|
|
147
163
|
// Download a source video across YouTube / TikTok / Douyin / Facebook.
|
|
148
164
|
// yt-dlp supports all of them, but a single rigid `-f` that works for
|
|
149
165
|
// YouTube fails on the others, so try a tolerant 720p-capped format then
|
|
@@ -153,6 +169,10 @@ async function downloadSourceVideo(url, dest) {
|
|
|
153
169
|
const platform = detectPlatform(url);
|
|
154
170
|
const common = [
|
|
155
171
|
'--no-playlist', '--no-warnings', '--retries', '3', '--fragment-retries', '3',
|
|
172
|
+
// dest ends in .mp4 — force merged/odd containers to mp4 so the file
|
|
173
|
+
// lands EXACTLY at `dest` (else fs.rename → ENOENT, e.g. YouTube shorts
|
|
174
|
+
// where bv*+ba merges to .mkv/.webm).
|
|
175
|
+
'--merge-output-format', 'mp4', '--remux-video', 'mp4',
|
|
156
176
|
'--user-agent', SRC_UA, ...cookieArgs(), '-o', dest, url,
|
|
157
177
|
];
|
|
158
178
|
const attempts = [
|
|
@@ -387,6 +407,15 @@ Rules:
|
|
|
387
407
|
}
|
|
388
408
|
// Single source of truth for the master-cast prompt block (used by Phase-1
|
|
389
409
|
// and the post-Phase-2 cast reconciliation so the format never drifts).
|
|
410
|
+
//
|
|
411
|
+
// The [CHARACTER CAST LIST] header carries CRITICAL RULES that tell the
|
|
412
|
+
// downstream image generator (Veo3 / FlowKit) how to render the cast. Rule
|
|
413
|
+
// #5 specifically prevents identity-morphing — even when the source video
|
|
414
|
+
// has siblings or look-alike characters, the generated reference sheet
|
|
415
|
+
// must give each character distinctly different facial structures so the
|
|
416
|
+
// per-scene Veo3 renders don't drift into the same face. This rule got
|
|
417
|
+
// lost in a prior refactor; restored to match the reference clone-tool
|
|
418
|
+
// (~/Downloads/if is_character_driven.txt).
|
|
390
419
|
function buildMasterCastPrompt(videoStyle, characters) {
|
|
391
420
|
if (!characters.length)
|
|
392
421
|
return '';
|
|
@@ -394,8 +423,15 @@ function buildMasterCastPrompt(videoStyle, characters) {
|
|
|
394
423
|
const castList = characters.map(c => `- ${c.name}: ${c.description}`).join('\n');
|
|
395
424
|
return (`[AESTHETIC & STYLE]\n${styleLine}\n` +
|
|
396
425
|
`[COMPOSITION & LAYOUT]\nCharacter Reference Sheet. Full-body side-by-side.\n` +
|
|
397
|
-
`[CHARACTER CAST LIST]
|
|
398
|
-
`
|
|
426
|
+
`[CHARACTER CAST LIST] (CRITICAL RULES: ` +
|
|
427
|
+
`1. Max 5 INDIVIDUAL characters. ` +
|
|
428
|
+
`2. STRICTLY PROHIBIT collective nouns, groups, crowds, or plurals. ` +
|
|
429
|
+
`3. If a group exists, pick ONLY the most prominent 1 or 2 individuals. ` +
|
|
430
|
+
`4. Detail unique physical appearance and clothing. ` +
|
|
431
|
+
`5. FORCE UNIQUE facial and physical structures for EVERY character ` +
|
|
432
|
+
`(assign distinct body types, face shapes, wrinkles, or accessories) ` +
|
|
433
|
+
`to prevent identity morphing. List with '- '. English.)\n${castList}\n` +
|
|
434
|
+
`[TECHNICAL SPECIFICATIONS]\nHigh detail, 8k resolution.`);
|
|
399
435
|
}
|
|
400
436
|
// Post-Phase-2 cast RECONCILE. Phase-1 only sees a 30-frame sample so it can
|
|
401
437
|
// miss a recurring character; the per-scene visionDescribe pass, however,
|
|
@@ -522,7 +558,7 @@ export async function analyzeVideo(url, onProgress) {
|
|
|
522
558
|
let source_title = '';
|
|
523
559
|
try {
|
|
524
560
|
const t = await run(YT_DLP, ['--skip-download', '--no-warnings', '--no-playlist', '--user-agent', SRC_UA, ...cookieArgs(), '--print', '%(title)s', url]);
|
|
525
|
-
source_title = (t.out
|
|
561
|
+
source_title = cleanSourceTitle(t.out);
|
|
526
562
|
}
|
|
527
563
|
catch { /* title is best-effort — analysis still proceeds without it */ }
|
|
528
564
|
progress('Đang tách audio...');
|