Skip to content
Open
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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
}
},
"zencoder.enableRepoIndexing": true
}
172 changes: 149 additions & 23 deletions actions/upload-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import OpenAI from "openai";

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
timeout: 300000, // 5 minutes timeout
maxRetries: 5, // Increase retries for better reliability
defaultHeaders: {
"Keep-Alive": "timeout=300", // Keep connection alive for 5 minutes
},
defaultQuery: {
"request-timeout": "300000", // Request timeout hint for the API
},
});

export async function transcribeUploadedFile(
Expand Down Expand Up @@ -36,15 +44,82 @@ export async function transcribeUploadedFile(
};
}

const response = await fetch(fileUrl);

try {
const transcriptions = await openai.audio.transcriptions.create({
model: "whisper-1",
file: response,
});
// Fetch with timeout and retry logic
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000); // Increase to 120 second timeout for fetch

// Add retry logic for fetch
const fetchWithRetry = async (
url: string,
options: RequestInit,
maxRetries = 3,
) => {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url, options);
} catch (error) {
console.log(`Fetch attempt ${i + 1} failed, retrying...`);
lastError = error;
// Exponential backoff
await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));
}
}
throw lastError;
};

const response = await fetchWithRetry(fileUrl, {
signal: controller.signal,
headers: {
Connection: "keep-alive",
"Keep-Alive": "timeout=120",
},
}).finally(() => clearTimeout(timeoutId));

if (!response.ok) {
throw new Error(
`Failed to fetch file: ${response.status} ${response.statusText}`,
);
}

// Process in chunks to avoid timeouts
console.log("Starting transcription for file:", fileName);
const startTime = Date.now();

// Add retry logic for OpenAI API calls
const callWithRetry = async (fn: any, maxRetries = 3) => {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
console.log(`API call attempt ${i + 1} failed, retrying...`, error);
lastError = error;
// Exponential backoff
await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));
}
}
throw lastError;
};

// Convert the response to a Blob which is compatible with OpenAI's API
const audioBlob = await response.blob();
// Create a File object from the Blob with the original filename
const audioFile = new File([audioBlob], fileName, { type: audioBlob.type });

const transcriptions = await callWithRetry(() =>
openai.audio.transcriptions.create({
model: "whisper-1",
file: audioFile,
// Add response_format parameter for more reliable processing
response_format: "json",
}),
);

const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`Transcription completed in ${duration}s`);

console.log({ transcriptions });
return {
success: true,
message: "File uploaded successfully!",
Expand All @@ -53,10 +128,32 @@ export async function transcribeUploadedFile(
} catch (error) {
console.error("Error processing file", error);

if (error instanceof OpenAI.APIError && error.status === 413) {
// Handle specific error types
if (error instanceof OpenAI.APIError) {
if (error.status === 413) {
return {
success: false,
message: "File size exceeds the max limit of 20MB",
data: null,
};
}

if (error.status === 504 || error.status === 408) {
return {
success: false,
message:
"The request timed out. Try with a shorter audio file or try again later.",
data: null,
};
}
}

// Handle fetch timeout
if (error instanceof Error && error.name === "AbortError") {
return {
success: false,
message: "File size exceeds the max limit of 20MB",
message:
"Request timed out while fetching the file. Please try again with a smaller file.",
data: null,
};
}
Expand Down Expand Up @@ -107,16 +204,38 @@ async function generateBlogPost({
transcriptions: string;
userPosts: string;
}) {
const completion = await openai.chat.completions.create({
messages: [
{
role: "system",
content:
"You are a skilled content writer that converts audio transcriptions into well-structured, engaging blog posts in Markdown format. Create a comprehensive blog post with a catchy title, introduction, main body with multiple sections, and a conclusion. Analyze the user's writing style from their previous posts and emulate their tone and style in the new post. Keep the tone casual and professional.",
},
// Add retry logic for OpenAI API calls
const callWithRetry = async (fn: any, maxRetries = 3) => {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
console.log(
`Blog generation attempt ${i + 1} failed, retrying...`,
error,
);
lastError = error;
// Exponential backoff
await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));
}
}
throw lastError;
};

// Stream the response to avoid timeouts
const completion = await callWithRetry(() =>
openai.chat.completions.create(
{
role: "user",
content: `Here are some of my previous blog posts for reference:
messages: [
{
role: "system",
content:
"You are a skilled content writer that converts audio transcriptions into well-structured, engaging blog posts in Markdown format. Create a comprehensive blog post with a catchy title, introduction, main body with multiple sections, and a conclusion. Analyze the user's writing style from their previous posts and emulate their tone and style in the new post. Keep the tone casual and professional.",
},
{
role: "user",
content: `Here are some of my previous blog posts for reference:

${userPosts}

Expand All @@ -133,12 +252,19 @@ Please convert the following transcription into a well-structured blog post usin
9. Emulate my writing style, tone, and any recurring patterns you notice from my previous posts.

Here's the transcription to convert: ${transcriptions}`,
},
],
model: "gpt-4o-mini",
temperature: 0.7,
max_tokens: 1000,
// Set stream to false to get the complete response at once
stream: false,
},
{
timeout: 180000, // 3 minutes timeout specifically for this call
},
],
model: "gpt-4o-mini",
temperature: 0.7,
max_tokens: 1000,
});
),
);

return completion.choices[0].message.content;
}
Expand Down
65 changes: 53 additions & 12 deletions components/upload/upload-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,46 +52,87 @@ export default function UploadForm({ planTypeName }: { planTypeName: string }) {
validatedFields.error.flatten().fieldErrors.file?.[0] ??
"Invalid file",
});
return;
}

if (file) {
// Upload the file
const resp: any = await startUpload([file]);
console.log({ resp });

if (!resp) {
toast.error("Something went wrong", {
description: "Please use a different file",
});
return;
}
toast.info("Transcription is in progress...", {

// Show a loading toast that stays visible during the potentially long transcription process
const toastId = toast.loading("Transcription is in progress...", {
description:
"Hang tight! Our digital wizards are sprinkling magic dust on your file! ✨",
"Hang tight! Our digital wizards are sprinkling magic dust on your file! ✨ This may take a few minutes for longer files.",
duration: 120000, // 2 minutes
});

const result = await transcribeUploadedFile(resp);
const { data = null, message = null } = result || {};
// Try to transcribe with better error handling
let result;
try {
result = await transcribeUploadedFile(resp);
// Dismiss the loading toast
toast.dismiss(toastId);
} catch (error) {
// Dismiss the loading toast
toast.dismiss(toastId);

console.error("Error during transcription:", error);
toast.error("Transcription process failed", {
description:
"The request may have timed out. Try with a shorter audio file or try again later.",
});
return;
}

const { data = null, message = null, success = false } = result || {};

// Handle transcription failures
if (!result || (!data && !message)) {
toast.error("An unexpected error occurred", {
description:
"An error occurred during transcription. Please try again.",
});
return;
}

if (!success) {
toast.error("Transcription failed", {
description: message || "Please try again with a different file.",
});
return;
}

// If we have data, proceed with blog post generation
if (data) {
toast.message("Generating AI blog post...", {
description: "Please wait while we generate your blog post.",
});

await generateBlogPostAction({
transcriptions: data.transcriptions,
userId: data.userId,
});
try {
await generateBlogPostAction({
transcriptions: data.transcriptions,
userId: data.userId,
});

toast.message("🎉 Woohoo! Your AI blog is created! 🎊", {
description:
"Time to put on your editor hat, Click the post and edit it!",
});
toast.message("🎉 Woohoo! Your AI blog is created! 🎊", {
description:
"Time to put on your editor hat, Click the post and edit it!",
});
} catch (error) {
console.error("Error generating blog post:", error);
toast.error("Failed to generate blog post", {
description:
"There was an error generating your blog post. Please try again.",
});
}
}
}
};
Expand Down
27 changes: 26 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
// Increase the server timeout for API routes and server actions
experimental: {
serverActions: {
// 5 minutes timeout for server actions
bodySizeLimit: "50mb",
allowedOrigins: ["localhost:3000", "writora.xyz", "www.writora.xyz"],
// Increase timeout for server actions
serverActionsTimeout: 300000, // 5 minutes in milliseconds
},
},
api: {
// 5 minutes timeout for API routes
responseLimit: false,
bodyParser: {
sizeLimit: "50mb",
},
// Increase timeout for API routes
externalResolver: true,
},
// Add production-specific optimizations
productionBrowserSourceMaps: false, // Disable source maps in production for better performance
poweredByHeader: false, // Remove the X-Powered-By header for security
// Increase general timeout
httpTimeout: 300000, // 5 minutes in milliseconds
};

export default nextConfig;
Loading