tuna-agent 0.1.148 → 0.1.149
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.
|
@@ -29,6 +29,8 @@ const RATES = {
|
|
|
29
29
|
'gpt-4o': { in: 2.50, out: 10.0 },
|
|
30
30
|
// Gemini 3 Flash preview: text+image input share one rate, output 6x.
|
|
31
31
|
'gemini-3-flash-preview': { in: 0.50, out: 3.0 },
|
|
32
|
+
// Gemini 2.5 Flash: cheaper image-heavy reads (used by Phase-1).
|
|
33
|
+
'gemini-2.5-flash': { in: 0.30, out: 2.50 },
|
|
32
34
|
};
|
|
33
35
|
// Per-run cost+token accumulator. Single-threaded JS → plain mutation is safe
|
|
34
36
|
// even across the parallel visionDescribe calls.
|
|
@@ -50,12 +52,12 @@ class CostTracker {
|
|
|
50
52
|
}
|
|
51
53
|
// Gemini reports usageMetadata.{promptTokenCount,candidatesTokenCount}
|
|
52
54
|
// instead of OpenAI's prompt_tokens/completion_tokens.
|
|
53
|
-
geminiVision(bucket, usage) {
|
|
55
|
+
geminiVision(bucket, usage, model = 'gemini-3-flash-preview') {
|
|
54
56
|
if (!usage) {
|
|
55
57
|
this.add(bucket, 0);
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
58
|
-
const r = RATES[
|
|
60
|
+
const r = RATES[model];
|
|
59
61
|
const cost = ((usage.promptTokenCount || 0) / 1e6) * r.in + ((usage.candidatesTokenCount || 0) / 1e6) * r.out;
|
|
60
62
|
this.add(bucket, cost);
|
|
61
63
|
}
|
|
@@ -174,7 +176,7 @@ ${rawText}`,
|
|
|
174
176
|
// One Gemini generateContent call with key rotation + exponential backoff on
|
|
175
177
|
// 429/5xx. A single free-tier key under the 5-way concurrent batch WILL
|
|
176
178
|
// rate-limit; retrying (slower) beats dropping the scene description.
|
|
177
|
-
async function geminiGenerate(parts, maxOutputTokens) {
|
|
179
|
+
async function geminiGenerate(parts, maxOutputTokens, model = GEMINI_MODEL) {
|
|
178
180
|
if (!GEMINI_KEYS.length)
|
|
179
181
|
return { text: '' };
|
|
180
182
|
const body = JSON.stringify({
|
|
@@ -187,7 +189,7 @@ async function geminiGenerate(parts, maxOutputTokens) {
|
|
|
187
189
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
188
190
|
const key = GEMINI_KEYS[keyIdx % GEMINI_KEYS.length];
|
|
189
191
|
try {
|
|
190
|
-
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(
|
|
192
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${key}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body });
|
|
191
193
|
if (res.status === 429 || res.status >= 500) {
|
|
192
194
|
lastErr = `Gemini ${res.status}`;
|
|
193
195
|
keyIdx++; // rotate to the next key before backing off
|
|
@@ -259,13 +261,10 @@ async function visionExtractPhase1(frames, transcript, cost) {
|
|
|
259
261
|
master_cast_prompt: '',
|
|
260
262
|
characters: [],
|
|
261
263
|
};
|
|
262
|
-
if (!
|
|
264
|
+
if (!GEMINI_KEYS.length || frames.length === 0)
|
|
263
265
|
return empty;
|
|
264
266
|
try {
|
|
265
|
-
const
|
|
266
|
-
{
|
|
267
|
-
type: 'text',
|
|
268
|
-
text: `Act as a Master Film Director. These frames are sampled across an ENTIRE video, in order. Use them + the transcript to analyse the whole piece.
|
|
267
|
+
const promptText = `Act as a Master Film Director. These frames are sampled across an ENTIRE video, in order. Use them + the transcript to analyse the whole piece.
|
|
269
268
|
|
|
270
269
|
Transcript context: "${(transcript || '').slice(0, 4000)}"
|
|
271
270
|
|
|
@@ -282,22 +281,18 @@ Rules:
|
|
|
282
281
|
- characters.name: a stable short uppercase label reused for this subject (e.g. "THE BISHOP", "U-94 SUBMARINE"). Max 4 words.
|
|
283
282
|
- RECALL (CRITICAL): list EVERY distinct recurring subject SEPARATELY. If a family or group recurs, include EACH member as its own entry (e.g. adult man, adult woman, older boy, younger girl) — never merge them into one. Skip only true one-off background extras. Be COMPLETE: missing a recurring character is worse than one extra. Up to 8.
|
|
284
283
|
- characters.description: ENGLISH only, factual, no camera/action words.
|
|
285
|
-
- DISTINCT FACES (CRITICAL): every character MUST have a HIGHLY UNIQUE facial structure, a distinct hairstyle, a specific body type and a clearly different age. NEVER reuse the same or a similar facial description for two characters — they must look completely different from one another
|
|
286
|
-
|
|
284
|
+
- DISTINCT FACES (CRITICAL): every character MUST have a HIGHLY UNIQUE facial structure, a distinct hairstyle, a specific body type and a clearly different age. NEVER reuse the same or a similar facial description for two characters — they must look completely different from one another.`;
|
|
285
|
+
// Phase-1 on Gemini 2.5 Flash: image-heavy read is far cheaper than gpt-4o,
|
|
286
|
+
// and cast recall is backstopped by the post-Phase-2 reconcile pass, so a
|
|
287
|
+
// small frame sample suffices here.
|
|
288
|
+
const parts = [
|
|
289
|
+
{ text: promptText },
|
|
290
|
+
...frames.map(b64 => ({ inlineData: { mimeType: 'image/jpeg', data: b64 } })),
|
|
287
291
|
];
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
292
|
-
method: 'POST',
|
|
293
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${OPENAI_KEY}` },
|
|
294
|
-
body: JSON.stringify({ model: 'gpt-4o', max_tokens: 1600, messages: [{ role: 'user', content }] }),
|
|
295
|
-
});
|
|
296
|
-
if (!res.ok)
|
|
292
|
+
const { text: rawTxt, usage } = await geminiGenerate(parts, 1600, 'gemini-2.5-flash');
|
|
293
|
+
if (!rawTxt)
|
|
297
294
|
return empty;
|
|
298
|
-
|
|
299
|
-
cost?.chat('phase1', 'gpt-4o', data.usage);
|
|
300
|
-
const rawTxt = (data.choices?.[0]?.message?.content || '').trim();
|
|
295
|
+
cost?.geminiVision('phase1', usage, 'gemini-2.5-flash');
|
|
301
296
|
let parsed = {};
|
|
302
297
|
try {
|
|
303
298
|
const m = rawTxt.match(/\{[\s\S]*\}/);
|
|
@@ -599,10 +594,12 @@ export async function analyzeVideo(url, onProgress) {
|
|
|
599
594
|
// master cast + characters. Runs before per-scene describe so the cast
|
|
600
595
|
// context keeps naming consistent across the whole timeline.
|
|
601
596
|
progress('Đang phân tích tổng thể (summary + style + master cast)...');
|
|
602
|
-
// Sample up to
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
|
|
597
|
+
// Sample up to 10 frames evenly — enough for summary + style + a naming
|
|
598
|
+
// seed. Cast RECALL no longer depends on this sample: the post-Phase-2
|
|
599
|
+
// reconcile pass derives the definitive cast from every per-scene
|
|
600
|
+
// description, so a small sample keeps the (now Gemini 2.5 Flash) Phase-1
|
|
601
|
+
// call cheap.
|
|
602
|
+
const p1SampleCount = Math.min(10, frameBuffers.length);
|
|
606
603
|
const p1Step = Math.max(1, Math.floor(frameBuffers.length / p1SampleCount));
|
|
607
604
|
const p1Samples = frameBuffers
|
|
608
605
|
.filter((_, i) => i % p1Step === 0)
|