Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion packages/backend/src/engine/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,52 @@ export class WorkflowValidator {
break;

case 'twitter':
case 'instagram':
case 'facebook':
case 'tiktok':
case 'youtube':
if (!node.data.authenticated) {
errors.push({
type: 'configuration',
nodeId: node.id,
message: 'Twitter node requires authentication'
message: `${node.data.type} node requires authentication`
});
}
break;

case 'wan2-video':
if (!node.data.model) {
errors.push({
type: 'configuration',
nodeId: node.id,
message: 'Wan2 Video node must specify a model'
});
}
if (!node.data.size) {
errors.push({
type: 'configuration',
nodeId: node.id,
message: 'Wan2 Video node must specify a video size'
});
}
break;

case 'prompt-enhancer-image':
case 'prompt-enhancer-video':
if (!node.data.userPrompt || node.data.userPrompt.trim() === '') {
errors.push({
type: 'configuration',
nodeId: node.id,
message: 'Prompt enhancer node must have non-empty user prompt'
});
}
break;

case 'vision-analyzer':
// Vision analyzer can have imageUrl, videoUrl, or uploadedFile
// No strict validation needed as it can be configured during execution
break;

default:
errors.push({
type: 'configuration',
Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import path from 'path';
import { ErrorResponse } from '@vlowgen/shared';
import workflowRouter from './api/workflows';

// Load environment variables
dotenv.config();
// Load environment variables from packages/backend/.env
dotenv.config({ path: path.join(__dirname, '../.env') });

const app = express();
const PORT = process.env.PORT || 3001;
Expand Down
63 changes: 51 additions & 12 deletions packages/backend/src/integrations/wan2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import axios, { AxiosInstance, AxiosError } from 'axios';
export interface Wan2GenerateParams {
prompt: string;
model: 'wanx-v1' | 'wanx-v2' | 'wan2.1-t2i-turbo' | 'wan2.1-t2i-plus' | 'wan2.6-t2i';
size: '1024x1024' | '512x512';
size: '1024*1024' | '512*512' | '720*1280' | '1280*720';
style?: string;
}

Expand All @@ -34,7 +34,7 @@ export class Wan2Client {
private client: AxiosInstance;
private apiKey: string;

constructor(apiKey: string, apiUrl: string = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis') {
constructor(apiKey: string, apiUrl: string = 'https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis') {
this.apiKey = apiKey;
this.client = axios.create({
baseURL: apiUrl,
Expand All @@ -47,14 +47,15 @@ export class Wan2Client {
}

/**
* Generate an image from a text prompt using Wan2.1 API
* Generate an image from a text prompt using Wan2.1 API (async with polling)
*
* @param params - Generation parameters including prompt, model, size, and optional style
* @returns Promise resolving to image URL and task ID
* @throws Error if API request fails or returns an error
*/
async generateImage(params: Wan2GenerateParams): Promise<Wan2GenerateResponse> {
try {
// Step 1: Create async task
const requestBody = {
model: params.model,
input: {
Expand All @@ -67,24 +68,58 @@ export class Wan2Client {
},
};

const response = await this.client.post<Wan2ApiResponse>('', requestBody);
const createResponse = await this.client.post<Wan2ApiResponse>('', requestBody, {
headers: {
'X-DashScope-Async': 'enable',
},
});

const taskId = createResponse.data.output.task_id;

// Step 2: Poll for result (max 2 minutes for image)
const maxAttempts = 40; // 40 attempts × 3 seconds = 2 minutes
const pollInterval = 3000; // 3 seconds

for (let attempt = 0; attempt < maxAttempts; attempt++) {
await this.sleep(pollInterval);

const statusResponse = await axios.get<Wan2ApiResponse>(
`https://dashscope-intl.aliyuncs.com/api/v1/tasks/${taskId}`,
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
},
}
);

const status = statusResponse.data.output.task_status;

// Extract image URL from response
if (!response.data.output?.results?.[0]?.url) {
throw new Error('Invalid API response: missing image URL');
if (status === 'SUCCEEDED') {
if (!statusResponse.data.output.results?.[0]?.url) {
throw new Error('Image generation succeeded but no image URL returned');
}

return {
imageUrl: statusResponse.data.output.results[0].url,
taskId,
};
} else if (status === 'FAILED') {
throw new Error('Image generation failed');
} else if (status === 'UNKNOWN') {
throw new Error('Task expired or not found');
}

// Continue polling if PENDING or RUNNING
}

return {
imageUrl: response.data.output.results[0].url,
taskId: response.data.output.task_id,
};
throw new Error('Image generation timeout after 2 minutes');
} catch (error) {
// Map API errors to standard error format
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;

if (axiosError.code === 'ECONNABORTED') {
throw new Error('Wan2.1 API request timeout after 60 seconds');
throw new Error('Wan2.1 API request timeout');
}

if (axiosError.response) {
Expand All @@ -103,4 +138,8 @@ export class Wan2Client {
throw error;
}
}

private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/nodes/ai/wan2-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export class Wan2NodeHandler implements NodeHandler {
// Get DashScope API key from environment (same key used for Qwen and Wan2)
const dashscopeApiKey = process.env.DASHSCOPE_API_KEY || context.credentials.wan2ApiKey;

console.log('[Wan2Handler] Using API key:', dashscopeApiKey ? dashscopeApiKey.substring(0, 10) + '...' : 'NOT FOUND');

if (!dashscopeApiKey) {
const endTime = new Date().toISOString();
return {
Expand Down
1 change: 0 additions & 1 deletion packages/frontend/.astro/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />
14 changes: 7 additions & 7 deletions packages/frontend/src/pages/api/composio/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const POST: APIRoute = async ({ request }) => {
);
}

const composioApiKey = import.meta.env.COMPOSIO_API_KEY;
const composioApiKey = process.env.COMPOSIO_API_KEY || import.meta.env.COMPOSIO_API_KEY;

if (!composioApiKey) {
return new Response(
Expand All @@ -26,11 +26,11 @@ export const POST: APIRoute = async ({ request }) => {

// Get auth config ID based on platform
const authConfigMap: Record<string, string | undefined> = {
twitter: import.meta.env.TWITTER_AUTH_CONFIG_ID,
facebook: import.meta.env.FACEBOOK_AUTH_CONFIG_ID,
instagram: import.meta.env.INSTAGRAM_AUTH_CONFIG_ID,
tiktok: import.meta.env.TIKTOK_AUTH_CONFIG_ID,
youtube: import.meta.env.YOUTUBE_AUTH_CONFIG_ID,
twitter: process.env.TWITTER_AUTH_CONFIG_ID || import.meta.env.TWITTER_AUTH_CONFIG_ID,
facebook: process.env.FACEBOOK_AUTH_CONFIG_ID || import.meta.env.FACEBOOK_AUTH_CONFIG_ID,
instagram: process.env.INSTAGRAM_AUTH_CONFIG_ID || import.meta.env.INSTAGRAM_AUTH_CONFIG_ID,
tiktok: process.env.TIKTOK_AUTH_CONFIG_ID || import.meta.env.TIKTOK_AUTH_CONFIG_ID,
youtube: process.env.YOUTUBE_AUTH_CONFIG_ID || import.meta.env.YOUTUBE_AUTH_CONFIG_ID,
};

const authConfigId = authConfigMap[platform.toLowerCase()];
Expand All @@ -45,7 +45,7 @@ export const POST: APIRoute = async ({ request }) => {
}

// Initiate connection with Composio using v3 API
const callbackUrl = `${import.meta.env.PUBLIC_API_URL || 'http://localhost:4321'}/api/composio/callback`;
const callbackUrl = `${process.env.PUBLIC_API_URL || import.meta.env.PUBLIC_API_URL || 'http://localhost:4321'}/api/composio/callback`;

const requestBody = {
auth_config: {
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/pages/api/composio/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const GET: APIRoute = async ({ request }) => {
);
}

const composioApiKey = import.meta.env.COMPOSIO_API_KEY;
const composioApiKey = process.env.COMPOSIO_API_KEY || import.meta.env.COMPOSIO_API_KEY;
if (!composioApiKey) {
return new Response(
JSON.stringify({ error: 'Composio API key not configured' }),
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/types/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface PromptTextNodeData {
export interface Wan2NodeData {
type: 'wan2';
model: 'wanx-v1' | 'wanx-v2' | 'wan2.1-t2i-turbo' | 'wan2.1-t2i-plus' | 'wan2.6-t2i';
size: '1024x1024' | '512x512';
size: '1024*1024' | '512*512' | '720*1280' | '1280*720';
style?: string;
}

Expand Down
88 changes: 88 additions & 0 deletions test-api-direct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Direct API Test - Test Wan2 API directly without backend
*/

const API_KEY = 'sk-466ebab0feed41f7880c3b7ca509d15b';
const BASE_URL = 'https://dashscope-intl.aliyuncs.com/api/v1';

async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async function testDirectAPI() {
console.log('🧪 Testing Wan2 API directly...\n');

// Step 1: Create task
console.log('📤 Step 1: Creating image generation task...');
const createResponse = await fetch(`${BASE_URL}/services/aigc/text2image/image-synthesis`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
'X-DashScope-Async': 'enable',
},
body: JSON.stringify({
model: 'wan2.1-t2i-turbo',
input: {
prompt: 'A beautiful sunset over Indonesian rice terraces'
},
parameters: {
size: '1024*1024',
n: 1
}
})
});

const createResult = await createResponse.json();
console.log('Response:', JSON.stringify(createResult, null, 2));

if (!createResult.output?.task_id) {
console.error('❌ Failed to create task');
return;
}

const taskId = createResult.output.task_id;
console.log(`✅ Task created: ${taskId}\n`);

// Step 2: Poll for result
console.log('⏳ Step 2: Polling for result (max 2 minutes)...');
const maxAttempts = 40;
const pollInterval = 3000;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await sleep(pollInterval);

console.log(` Attempt ${attempt}/${maxAttempts}...`);

const statusResponse = await fetch(`${BASE_URL}/tasks/${taskId}`, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
}
});

const statusResult = await statusResponse.json();
const status = statusResult.output?.task_status;

console.log(` Status: ${status}`);

if (status === 'SUCCEEDED') {
console.log('\n✅ Image generated successfully!\n');
console.log('📊 Result:');
console.log(JSON.stringify(statusResult, null, 2));

if (statusResult.output?.results?.[0]?.url) {
console.log('\n🖼️ Image URL:', statusResult.output.results[0].url);
console.log('📥 Download: curl -o generated-image.jpg "' + statusResult.output.results[0].url + '"');
}
return;
} else if (status === 'FAILED') {
console.error('\n❌ Task failed');
console.error(JSON.stringify(statusResult, null, 2));
return;
}
}

console.error('\n❌ Timeout after 2 minutes');
}

testDirectAPI().catch(console.error);
Loading
Loading