tuna-agent 0.1.37 → 0.1.39
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.
|
@@ -26,8 +26,8 @@ export interface VideoRecord {
|
|
|
26
26
|
aspectRatio?: string;
|
|
27
27
|
}
|
|
28
28
|
export declare function handleGetHistory(ws: AgentWebSocketClient, code: string, taskId: string): void;
|
|
29
|
-
export declare function handleGenerateIdeas(ws: AgentWebSocketClient, code: string, taskId: string, topic: string, styleName?: string, styleDesc?: string, language?: string, count?: number, ideasInstruction?: string): Promise<void>;
|
|
30
|
-
export declare function handleGenerateScript(ws: AgentWebSocketClient, code: string, taskId: string, idea: string, topic: string, style?: string, duration?: number, language?: string, styleName?: string, styleGuidance?: string): Promise<void>;
|
|
29
|
+
export declare function handleGenerateIdeas(ws: AgentWebSocketClient, code: string, taskId: string, topic: string, styleName?: string, styleDesc?: string, language?: string, count?: number, ideasInstruction?: string, provider?: string): Promise<void>;
|
|
30
|
+
export declare function handleGenerateScript(ws: AgentWebSocketClient, code: string, taskId: string, idea: string, topic: string, style?: string, duration?: number, language?: string, styleName?: string, styleGuidance?: string, provider?: string): Promise<void>;
|
|
31
31
|
export declare function handleRetryVideo(ws: AgentWebSocketClient, code: string, taskId: string, videoId: string): void;
|
|
32
32
|
export declare function handleGenerateScene(ws: AgentWebSocketClient, code: string, taskId: string, sceneIdx: number, prompt: string, aspectRatio: string): Promise<void>;
|
|
33
33
|
export declare function handleGenerateScenes(ws: AgentWebSocketClient, code: string, taskId: string, scenes: Array<{
|
|
@@ -15,6 +15,7 @@ import { execFile } from 'child_process';
|
|
|
15
15
|
import { promisify } from 'util';
|
|
16
16
|
import { pipeline } from 'stream/promises';
|
|
17
17
|
import { runClaude } from '../utils/claude-cli.js';
|
|
18
|
+
import { runOpenAI } from '../utils/openai-api.js';
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
20
|
// ─── Storage ──────────────────────────────────────────────────────────────────
|
|
20
21
|
const VIDEOS_DIR = path.join(os.homedir(), '.tuna-content', 'videos');
|
|
@@ -64,9 +65,9 @@ function hasContentCreator() {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
// ─── Handler: generate_ideas ──────────────────────────────────────────────────
|
|
67
|
-
export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, styleDesc, language, count, ideasInstruction) {
|
|
68
|
-
console.log(`[generate_ideas] Received: topic="${topic}" count=${count} ideasInstruction=${ideasInstruction ? ideasInstruction.length + ' chars' : '(none)'} styleName=${styleName || '(none)'}`);
|
|
69
|
-
if (!hasContentCreator()) {
|
|
68
|
+
export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, styleDesc, language, count, ideasInstruction, provider) {
|
|
69
|
+
console.log(`[generate_ideas] Received: topic="${topic}" count=${count} provider=${provider || 'claude-cli'} ideasInstruction=${ideasInstruction ? ideasInstruction.length + ' chars' : '(none)'} styleName=${styleName || '(none)'}`);
|
|
70
|
+
if (provider !== 'openai' && !hasContentCreator()) {
|
|
70
71
|
const error = 'content-creator agent not found on this machine';
|
|
71
72
|
console.error(`[generate_ideas] ${error}`);
|
|
72
73
|
ws.sendExtensionEvent(code, { type: 'ideas_result', ideas: [], error });
|
|
@@ -109,21 +110,30 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
|
|
|
109
110
|
`Example format: ["title 1","title 2","title 3","title 4","title 5"]`,
|
|
110
111
|
].join('\n');
|
|
111
112
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
let resultText;
|
|
114
|
+
if (provider === 'openai') {
|
|
115
|
+
console.log(`[generate_ideas] Using OpenAI provider`);
|
|
116
|
+
const oaiResult = await runOpenAI({ prompt, systemPrompt, timeoutMs: 60000 });
|
|
117
|
+
resultText = oaiResult.result;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const result = await runClaude({
|
|
121
|
+
prompt,
|
|
122
|
+
systemPrompt,
|
|
123
|
+
cwd: CONTENT_CREATOR_DIR,
|
|
124
|
+
maxTurns: 1,
|
|
125
|
+
outputFormat: 'json',
|
|
126
|
+
timeoutMs: 60000,
|
|
127
|
+
});
|
|
128
|
+
resultText = result.result;
|
|
129
|
+
}
|
|
120
130
|
let ideas = [];
|
|
121
131
|
try {
|
|
122
|
-
// Try all JSON arrays in the response and pick the one
|
|
132
|
+
// Try all JSON arrays in the response and pick the one closest to n items
|
|
123
133
|
const arrays = [];
|
|
124
134
|
const re = /\[(?:[^\[\]]*(?:"(?:[^"\\]|\\.)*"[^\[\]]*)*)\]/g;
|
|
125
135
|
let m;
|
|
126
|
-
while ((m = re.exec(
|
|
136
|
+
while ((m = re.exec(resultText)) !== null) {
|
|
127
137
|
try {
|
|
128
138
|
const parsed = JSON.parse(m[0]);
|
|
129
139
|
if (Array.isArray(parsed) && parsed.every(i => typeof i === 'string')) {
|
|
@@ -140,7 +150,7 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
|
|
|
140
150
|
catch { /* fall through to fallback */ }
|
|
141
151
|
// Fallback: split numbered/bulleted lines
|
|
142
152
|
if (ideas.length === 0) {
|
|
143
|
-
ideas =
|
|
153
|
+
ideas = resultText
|
|
144
154
|
.split('\n')
|
|
145
155
|
.map(l => l.replace(/^[\d\-\*\.\)\s]+/, '').replace(/^[""]|[""]$/g, '').trim())
|
|
146
156
|
.filter(l => l.length > 10 && !l.startsWith('[') && !l.toLowerCase().includes('json'))
|
|
@@ -168,8 +178,9 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
|
|
|
168
178
|
// 2. Run `claude -p <prompt> --append-system-prompt <template>` in lightweight mode
|
|
169
179
|
// 3. Stream text deltas back to extension
|
|
170
180
|
// 4. Return full script on completion
|
|
171
|
-
export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language, styleName, styleGuidance) {
|
|
172
|
-
|
|
181
|
+
export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language, styleName, styleGuidance, provider) {
|
|
182
|
+
console.log(`[generate_script] provider=${provider || 'claude-cli'}`);
|
|
183
|
+
if (provider !== 'openai' && !hasContentCreator()) {
|
|
173
184
|
const error = 'content-creator agent not found on this machine';
|
|
174
185
|
console.error(`[generate_script] ${error}`);
|
|
175
186
|
ws.sendExtensionDone(code, taskId, { error });
|
|
@@ -222,58 +233,76 @@ export async function handleGenerateScript(ws, code, taskId, idea, topic, style,
|
|
|
222
233
|
const systemPrompt = expandedTemplate + styleContext + (characterContext
|
|
223
234
|
? `\n\n## AVAILABLE CHARACTERS\n${characterContext}`
|
|
224
235
|
: '');
|
|
236
|
+
const userPrompt = `Viết script video theo hướng dẫn trên.\n\nIdea: ${idea}\nStyle: ${resolvedStyle}\nClips: ${Math.round(resolvedDuration / 8)} (each exactly 8s, total ~${resolvedDuration}s)\nLanguage: ${resolvedLanguage}`;
|
|
225
237
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
238
|
+
if (provider === 'openai') {
|
|
239
|
+
console.log(`[generate_script] Using OpenAI provider (streaming)`);
|
|
240
|
+
let sentTextLen = 0;
|
|
241
|
+
const oaiResult = await runOpenAI({
|
|
242
|
+
prompt: userPrompt,
|
|
243
|
+
systemPrompt,
|
|
244
|
+
timeoutMs: 180000,
|
|
245
|
+
stream: true,
|
|
246
|
+
onChunk: (chunk) => {
|
|
247
|
+
sentTextLen += chunk.length;
|
|
248
|
+
ws.sendExtensionStream(code, taskId, chunk);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
console.log(`[generate_script] OpenAI done: streamed=${sentTextLen} chars`);
|
|
252
|
+
const scriptText = oaiResult.result;
|
|
253
|
+
ws.sendExtensionDone(code, taskId, { script: scriptText, text: scriptText });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
let streamChunks = 0;
|
|
257
|
+
let sentTextLen = 0;
|
|
258
|
+
const result = await runClaude({
|
|
259
|
+
prompt: userPrompt,
|
|
260
|
+
systemPrompt,
|
|
261
|
+
cwd: CONTENT_CREATOR_DIR,
|
|
262
|
+
maxTurns: 4,
|
|
263
|
+
outputFormat: 'stream-json',
|
|
264
|
+
includePartialMessages: true,
|
|
265
|
+
timeoutMs: 180000,
|
|
266
|
+
onStreamLine: (data) => {
|
|
267
|
+
if (streamChunks < 5) {
|
|
268
|
+
const sub = data.subtype || '';
|
|
269
|
+
console.log(`[generate_script] stream #${streamChunks}: type=${data.type} sub=${sub}`);
|
|
251
270
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const block of content) {
|
|
261
|
-
if (block.type === 'text' && typeof block.text === 'string') {
|
|
262
|
-
fullText += block.text;
|
|
271
|
+
streamChunks++;
|
|
272
|
+
if (data.type === 'stream_event') {
|
|
273
|
+
const evt = data.event;
|
|
274
|
+
if (evt?.type === 'content_block_delta') {
|
|
275
|
+
const delta = evt.delta;
|
|
276
|
+
if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
|
|
277
|
+
sentTextLen += delta.text.length;
|
|
278
|
+
ws.sendExtensionStream(code, taskId, delta.text);
|
|
263
279
|
}
|
|
264
280
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (data.type === 'assistant') {
|
|
284
|
+
const msg = data.message;
|
|
285
|
+
const content = (msg?.content ?? data.content);
|
|
286
|
+
if (Array.isArray(content)) {
|
|
287
|
+
let fullText = '';
|
|
288
|
+
for (const block of content) {
|
|
289
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
290
|
+
fullText += block.text;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (fullText.length > sentTextLen) {
|
|
294
|
+
const delta = fullText.slice(sentTextLen);
|
|
295
|
+
sentTextLen = fullText.length;
|
|
296
|
+
ws.sendExtensionStream(code, taskId, delta);
|
|
297
|
+
}
|
|
269
298
|
}
|
|
270
299
|
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
console.log(`[generate_script] Done: ${streamChunks} chunks, streamed=${sentTextLen} chars, result=${result.result?.length || 0} chars`);
|
|
303
|
+
const scriptText = result.result;
|
|
304
|
+
ws.sendExtensionDone(code, taskId, { script: scriptText, text: scriptText });
|
|
305
|
+
}
|
|
277
306
|
}
|
|
278
307
|
catch (err) {
|
|
279
308
|
const error = err instanceof Error ? err.message : String(err);
|
package/dist/daemon/index.js
CHANGED
|
@@ -367,13 +367,13 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
367
367
|
}
|
|
368
368
|
if (extTask === 'generate_ideas') {
|
|
369
369
|
(async () => {
|
|
370
|
-
await handleGenerateIdeas(ws, extCode, extTaskId, msg.topic, msg.styleName, msg.styleDesc, msg.language, msg.count, msg.ideasInstruction);
|
|
370
|
+
await handleGenerateIdeas(ws, extCode, extTaskId, msg.topic, msg.styleName, msg.styleDesc, msg.language, msg.count, msg.ideasInstruction, msg.provider);
|
|
371
371
|
})();
|
|
372
372
|
break;
|
|
373
373
|
}
|
|
374
374
|
if (extTask === 'generate_script') {
|
|
375
375
|
(async () => {
|
|
376
|
-
await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style, msg.duration, msg.language, msg.styleName, (msg.scriptInstruction || msg.styleGuidance));
|
|
376
|
+
await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style, msg.duration, msg.language, msg.styleName, (msg.scriptInstruction || msg.styleGuidance), msg.provider);
|
|
377
377
|
})();
|
|
378
378
|
break;
|
|
379
379
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface OpenAIRequest {
|
|
2
|
+
prompt: string;
|
|
3
|
+
systemPrompt?: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
maxTokens?: number;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
stream?: boolean;
|
|
8
|
+
onChunk?: (chunk: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface OpenAIResult {
|
|
11
|
+
result: string;
|
|
12
|
+
model: string;
|
|
13
|
+
usage?: {
|
|
14
|
+
prompt_tokens: number;
|
|
15
|
+
completion_tokens: number;
|
|
16
|
+
total_tokens: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export declare function runOpenAI(opts: OpenAIRequest): Promise<OpenAIResult>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// OpenAI API utility — direct fetch, no SDK needed
|
|
2
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || '';
|
|
3
|
+
const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4o';
|
|
4
|
+
export async function runOpenAI(opts) {
|
|
5
|
+
const { prompt, systemPrompt, model = OPENAI_MODEL, maxTokens = 4096, timeoutMs = 120000, stream = false, onChunk, } = opts;
|
|
6
|
+
if (!OPENAI_API_KEY) {
|
|
7
|
+
throw new Error('OPENAI_API_KEY environment variable is not set');
|
|
8
|
+
}
|
|
9
|
+
const messages = [];
|
|
10
|
+
if (systemPrompt) {
|
|
11
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
12
|
+
}
|
|
13
|
+
messages.push({ role: 'user', content: prompt });
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
+
try {
|
|
17
|
+
if (stream && onChunk) {
|
|
18
|
+
return await _streamRequest(messages, model, maxTokens, controller, onChunk);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return await _normalRequest(messages, model, maxTokens, controller);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function _normalRequest(messages, model, maxTokens, controller) {
|
|
29
|
+
const resp = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({ model, messages, max_tokens: maxTokens }),
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
});
|
|
38
|
+
if (!resp.ok) {
|
|
39
|
+
const body = await resp.text();
|
|
40
|
+
throw new Error(`OpenAI API error ${resp.status}: ${body}`);
|
|
41
|
+
}
|
|
42
|
+
const data = await resp.json();
|
|
43
|
+
const choice = data.choices?.[0];
|
|
44
|
+
return {
|
|
45
|
+
result: choice?.message?.content || '',
|
|
46
|
+
model: data.model,
|
|
47
|
+
usage: data.usage,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function _streamRequest(messages, model, maxTokens, controller, onChunk) {
|
|
51
|
+
const resp = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({ model, messages, max_tokens: maxTokens, stream: true }),
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
});
|
|
60
|
+
if (!resp.ok) {
|
|
61
|
+
const body = await resp.text();
|
|
62
|
+
throw new Error(`OpenAI API error ${resp.status}: ${body}`);
|
|
63
|
+
}
|
|
64
|
+
let fullText = '';
|
|
65
|
+
const reader = resp.body.getReader();
|
|
66
|
+
const decoder = new TextDecoder();
|
|
67
|
+
let buffer = '';
|
|
68
|
+
while (true) {
|
|
69
|
+
const { done, value } = await reader.read();
|
|
70
|
+
if (done)
|
|
71
|
+
break;
|
|
72
|
+
buffer += decoder.decode(value, { stream: true });
|
|
73
|
+
const lines = buffer.split('\n');
|
|
74
|
+
buffer = lines.pop() || '';
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (!trimmed || !trimmed.startsWith('data: '))
|
|
78
|
+
continue;
|
|
79
|
+
const payload = trimmed.slice(6);
|
|
80
|
+
if (payload === '[DONE]')
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(payload);
|
|
84
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
85
|
+
if (delta) {
|
|
86
|
+
fullText += delta;
|
|
87
|
+
onChunk(delta);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { /* skip parse errors */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { result: fullText, model };
|
|
94
|
+
}
|