tuna-agent 0.1.36 → 0.1.38
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,8 +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
|
-
|
|
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()) {
|
|
69
71
|
const error = 'content-creator agent not found on this machine';
|
|
70
72
|
console.error(`[generate_ideas] ${error}`);
|
|
71
73
|
ws.sendExtensionEvent(code, { type: 'ideas_result', ideas: [], error });
|
|
@@ -108,21 +110,30 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
|
|
|
108
110
|
`Example format: ["title 1","title 2","title 3","title 4","title 5"]`,
|
|
109
111
|
].join('\n');
|
|
110
112
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
130
|
let ideas = [];
|
|
120
131
|
try {
|
|
121
|
-
// 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
|
|
122
133
|
const arrays = [];
|
|
123
134
|
const re = /\[(?:[^\[\]]*(?:"(?:[^"\\]|\\.)*"[^\[\]]*)*)\]/g;
|
|
124
135
|
let m;
|
|
125
|
-
while ((m = re.exec(
|
|
136
|
+
while ((m = re.exec(resultText)) !== null) {
|
|
126
137
|
try {
|
|
127
138
|
const parsed = JSON.parse(m[0]);
|
|
128
139
|
if (Array.isArray(parsed) && parsed.every(i => typeof i === 'string')) {
|
|
@@ -139,7 +150,7 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
|
|
|
139
150
|
catch { /* fall through to fallback */ }
|
|
140
151
|
// Fallback: split numbered/bulleted lines
|
|
141
152
|
if (ideas.length === 0) {
|
|
142
|
-
ideas =
|
|
153
|
+
ideas = resultText
|
|
143
154
|
.split('\n')
|
|
144
155
|
.map(l => l.replace(/^[\d\-\*\.\)\s]+/, '').replace(/^[""]|[""]$/g, '').trim())
|
|
145
156
|
.filter(l => l.length > 10 && !l.startsWith('[') && !l.toLowerCase().includes('json'))
|
|
@@ -167,8 +178,9 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
|
|
|
167
178
|
// 2. Run `claude -p <prompt> --append-system-prompt <template>` in lightweight mode
|
|
168
179
|
// 3. Stream text deltas back to extension
|
|
169
180
|
// 4. Return full script on completion
|
|
170
|
-
export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language, styleName, styleGuidance) {
|
|
171
|
-
|
|
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()) {
|
|
172
184
|
const error = 'content-creator agent not found on this machine';
|
|
173
185
|
console.error(`[generate_script] ${error}`);
|
|
174
186
|
ws.sendExtensionDone(code, taskId, { error });
|
|
@@ -221,58 +233,76 @@ export async function handleGenerateScript(ws, code, taskId, idea, topic, style,
|
|
|
221
233
|
const systemPrompt = expandedTemplate + styleContext + (characterContext
|
|
222
234
|
? `\n\n## AVAILABLE CHARACTERS\n${characterContext}`
|
|
223
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}`;
|
|
224
237
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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}`);
|
|
250
270
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
for (const block of content) {
|
|
260
|
-
if (block.type === 'text' && typeof block.text === 'string') {
|
|
261
|
-
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);
|
|
262
279
|
}
|
|
263
280
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
}
|
|
268
298
|
}
|
|
269
299
|
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
}
|
|
276
306
|
}
|
|
277
307
|
catch (err) {
|
|
278
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
|
+
}
|