-
Notifications
You must be signed in to change notification settings - Fork 0
feat: a working first mvp of the project #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| "use server" | ||
|
|
||
| import { execFile } from "child_process"; | ||
| import { promisify } from "util"; | ||
|
|
||
| const execFileAsync = promisify(execFile); | ||
|
|
||
| export async function mp3ToWav(mp3Path: string, wavPath: string) { | ||
| await execFileAsync("ffmpeg", [ | ||
| "-y", | ||
| "-i", mp3Path, | ||
| "-ac", "1", | ||
| "-ar", "16000", | ||
| "-c:a", "pcm_s16le", | ||
| wavPath, | ||
| ]); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,102 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // app/api/render-video/route.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { renderMedia, selectComposition } from "@remotion/renderer"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from "fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Caption } from "@remotion/captions"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cleanOldCache, getCachedVideo } from "@/lib/video-cache"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(request: Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const renderData = await request.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // load captions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let captions: Caption[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (renderData.captionsPath) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const captionsFullPath = path.join(process.cwd(), renderData.captionsPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const captionsContent = fs.readFileSync(captionsFullPath, "utf-8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| captions = JSON.parse(captionsContent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Error loading captions:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Failed to load captions" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Block path traversal when loading captions. 🔒 Proposed fix- const captionsFullPath = path.join(process.cwd(), renderData.captionsPath);
+ const captionsBaseDir = path.join(process.cwd(), "public", "captions");
+ const captionsFullPath = path.resolve(
+ captionsBaseDir,
+ renderData.captionsPath
+ );
+ if (!captionsFullPath.startsWith(captionsBaseDir + path.sep)) {
+ return Response.json(
+ { success: false, error: "Invalid captions path" },
+ { status: 400 }
+ );
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Download and cache background video if it's from Cloudinary | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let videoUrl = renderData.videoUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (videoUrl && videoUrl.includes("cloudinary.com")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| videoUrl = await getCachedVideo(videoUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Using cached video:", videoUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Failed to cache video:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Failed to download background video" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate Cloudinary URLs with URL parsing to avoid SSRF bypasses. 🛡️ Proposed fix- if (videoUrl && videoUrl.includes("cloudinary.com")) {
- try {
- videoUrl = await getCachedVideo(videoUrl);
- console.log("Using cached video:", videoUrl);
- } catch (error) {
- console.error("Failed to cache video:", error);
- return Response.json(
- { success: false, error: "Failed to download background video" },
- { status: 500 }
- );
- }
- }
+ if (videoUrl) {
+ let isCloudinary = false;
+ try {
+ const parsed = new URL(videoUrl);
+ isCloudinary =
+ parsed.protocol === "https:" &&
+ (parsed.hostname === "res.cloudinary.com" ||
+ parsed.hostname.endsWith(".cloudinary.com"));
+ } catch {
+ // Not an absolute URL → treat as local path
+ }
+
+ if (isCloudinary) {
+ try {
+ videoUrl = await getCachedVideo(videoUrl);
+ console.log("Using cached video:", videoUrl);
+ } catch (error) {
+ console.error("Failed to cache video:", error);
+ return Response.json(
+ { success: false, error: "Failed to download background video" },
+ { status: 500 }
+ );
+ }
+ } else if (/^https?:\/\//i.test(videoUrl)) {
+ return Response.json(
+ { success: false, error: "Unsupported remote video URL" },
+ { status: 400 }
+ );
+ }
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create the final input props | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const inputProps = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hook: renderData.hook, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| story: renderData.story, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| audioUrl: renderData.audioUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| videoUrl: videoUrl, // Use cached local path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| highlightColor: renderData.highlightColor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| captions: captions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bundleLocation = path.join(process.cwd(), "build"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const outputDir = path.join(process.cwd(), "public", "videos"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!fs.existsSync(outputDir)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.mkdirSync(outputDir, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const composition = await selectComposition({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| serveUrl: bundleLocation, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: "MyVideo", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputProps: inputProps, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeoutInMilliseconds: 60000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lastCaptionEndMs = captions.length | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? Math.max(...captions.map(c => c.endMs)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 2000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const durationInFrames = Math.ceil((lastCaptionEndMs / 1000) * 30); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // override duration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| composition.durationInFrames = durationInFrames; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n app/api/render-video/route.ts | sed -n '59,75p'Repository: subhraneel2005/vidgen Length of output: 609 🌐 Web query:
💡 Result: To “override” a composition’s
Citations:
🏁 Script executed: # Check if there's a calculateMetadata callback defined in the composition setup
grep -r "calculateMetadata" app/ --include="*.ts" --include="*.tsx"Repository: subhraneel2005/vidgen Length of output: 47 🌐 Web query:
💡 Result: In Also note: Remotion config ( Example (get
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | |
| const composition = await selectComposition({ | |
| serveUrl: bundleLocation, | |
| id: "MyVideo", | |
| inputProps: inputProps, | |
| timeoutInMilliseconds: 60000, | |
| }); | |
| const lastCaptionEndMs = captions.length | |
| ? Math.max(...captions.map(c => c.endMs)) | |
| : 2000; | |
| const durationInFrames = Math.ceil((lastCaptionEndMs / 1000) * 30); | |
| // override duration | |
| composition.durationInFrames = durationInFrames; | |
| try { | |
| const composition = await selectComposition({ | |
| serveUrl: bundleLocation, | |
| id: "MyVideo", | |
| inputProps: inputProps, | |
| timeoutInMilliseconds: 60000, | |
| }); | |
| const lastCaptionEndMs = captions.length | |
| ? Math.max(...captions.map(c => c.endMs)) | |
| : 2000; | |
| const durationInFrames = lastCaptionEndMs | |
| ? Math.ceil((lastCaptionEndMs / 1000) * composition.fps) | |
| : composition.durationInFrames; | |
🤖 Prompt for AI Agents
In `@app/api/render-video/route.ts` around lines 59 - 75, The code computes
durationInFrames using a hardcoded 30fps and mutates
composition.durationInFrames after selectComposition; instead, move the
caption-based duration logic into the composition definition via
calculateMetadata so selectComposition returns correct metadata, or if you
cannot change the composition, compute frames using composition.fps (from the
returned composition) and avoid directly mutating
composition.durationInFrames—use the calculateMetadata hook on the <Composition>
(or a separate metadata calculation before rendering) to derive duration from
captions (use captions.length to handle missing captions and compute
lastCaptionEndMs safely).
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,31 @@ | ||||||||||||||||||||||||||||||||||||||||||
| import { RenderButton } from "@/components/render-button"; | ||||||||||||||||||||||||||||||||||||||||||
| import ChooseHighlightColor from "@/components/screens/choose-highlight-color"; | ||||||||||||||||||||||||||||||||||||||||||
| import ChooseBGVideo from "@/components/screens/choose-stock-bg"; | ||||||||||||||||||||||||||||||||||||||||||
| import GenerateAudio from "@/components/screens/generate-audio-screen"; | ||||||||||||||||||||||||||||||||||||||||||
| import GenerateCaptions from "@/components/screens/generate-captions-screen"; | ||||||||||||||||||||||||||||||||||||||||||
| import ScriptGenerator from "@/components/screens/script-generator"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export default function Page() { | ||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||
| <div className="flex-1 mt-6 ml-6 space-y-6"> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| {/* correct */} | ||||||||||||||||||||||||||||||||||||||||||
| <ScriptGenerator /> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| {/* correct */} | ||||||||||||||||||||||||||||||||||||||||||
| <GenerateAudio /> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| {/* incorrect, captions are getting generated manually r9*/} | ||||||||||||||||||||||||||||||||||||||||||
| <GenerateCaptions /> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| {/* correct */} | ||||||||||||||||||||||||||||||||||||||||||
| <ChooseBGVideo /> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| {/* correct */} | ||||||||||||||||||||||||||||||||||||||||||
| <ChooseHighlightColor /> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+12
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Remove debug/internal comments before merging. The inline comments ( The comment on line 19 ( 🧹 Proposed cleanup-
- {/* correct */}
<ScriptGenerator />
-
- {/* correct */}
<GenerateAudio />
-
- {/* incorrect, captions are getting generated manually r9*/}
<GenerateCaptions />
-
- {/* correct */}
<ChooseBGVideo />
-
- {/* correct */}
<ChooseHighlightColor />📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| <RenderButton /> | ||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| 'use client'; | ||
|
|
||
| import { useVideoStoryStore } from '@/store/useVideoStoryStore'; | ||
| import { useState } from 'react'; | ||
| import { Button } from './ui/button'; | ||
|
|
||
| export function RenderButton() { | ||
| const [isRendering, setIsRendering] = useState(false); | ||
| const getRenderData = useVideoStoryStore(state => state.getRenderData); | ||
|
|
||
| const handleRender = async () => { | ||
| setIsRendering(true); | ||
|
|
||
| // get plain json from zustand | ||
| const renderData = getRenderData(); | ||
|
|
||
| // send to /api/render-video | ||
| const response = await fetch('/api/render-video', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(renderData) // <-- this is plain json data no zustand | ||
| }); | ||
|
|
||
| const result = await response.json(); | ||
| console.log('Video rendered:', result.videoPath); | ||
|
|
||
| setIsRendering(false); | ||
| }; | ||
|
Comment on lines
+11
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling and user feedback. The fetch call has no error handling. Network failures, server errors (non-2xx responses), or JSON parse failures will cause unhandled exceptions, leaving 🐛 Proposed fix with proper error handling+import { toast } from 'sonner';
+
// ...
const handleRender = async () => {
setIsRendering(true);
-
- // get plain json from zustand
- const renderData = getRenderData();
-
- // send to /api/render-video
- const response = await fetch('/api/render-video', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(renderData) // <-- this is plain json data no zustand
- });
-
- const result = await response.json();
- console.log('Video rendered:', result.videoPath);
-
- setIsRendering(false);
+ try {
+ const renderData = getRenderData();
+
+ const response = await fetch('/api/render-video', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(renderData),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Render failed: ${response.status}`);
+ }
+
+ const result = await response.json();
+ console.log('Video rendered:', result.videoPath);
+ toast.success('Video rendered successfully!');
+ } catch (error) {
+ console.error('Render error:', error);
+ toast.error('Failed to render video');
+ } finally {
+ setIsRendering(false);
+ }
};🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <Button onClick={handleRender} disabled={isRendering}> | ||
| {isRendering ? 'Rendering...' : 'Generate Final Video'} | ||
| </Button> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import { useVideoStoryStore } from "@/store/useVideoStoryStore"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { toast } from "sonner"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from "../ui/button"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { generateCaptions } from "@/remotion/scripts/generate-captions"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export default function GenerateCaptions() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const { audioUrl, setCaptionsPath, captionsPath } = useVideoStoryStore(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const handleGenerateCaptions = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!audioUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| toast.error("Generate audio first"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const { captionsPath } = await generateCaptions(`public/${audioUrl}`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setCaptionsPath(captionsPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||
| toast.error("Failed to generate captions"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential path construction issue and error swallowing.
🔧 Suggested improvement setLoading(true);
try {
- const { captionsPath } = await generateCaptions(`public/${audioUrl}`);
+ // Ensure audioUrl doesn't already have public/ prefix
+ const normalizedPath = audioUrl.startsWith('public/')
+ ? audioUrl
+ : `public/${audioUrl}`;
+ const { captionsPath } = await generateCaptions(normalizedPath);
setCaptionsPath(captionsPath);
- } catch {
+ } catch (error) {
+ console.error("Caption generation failed:", error);
toast.error("Failed to generate captions");
} finally {
setLoading(false);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-full max-w-xl border border-border rounded-lg p-4 bg-secondary"> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="font-bold text-2xl">Generate captions</span> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| {audioUrl ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleGenerateCaptions} | ||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={loading} | ||||||||||||||||||||||||||||||||||||||||||||||||
| className="mt-3" | ||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||
| {loading ? "Generating..." : "Generate Captions"} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-yellow-500 mt-3">Generate audio first</p> | ||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| {captionsPath && ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="mt-2 text-sm text-green-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||
| Captions saved at: {captionsPath} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,37 @@ | ||
| "use server" | ||
|
|
||
| import { toCaptions, transcribe } from "@remotion/install-whisper-cpp"; | ||
| import path from "path"; | ||
| import fs from "fs"; | ||
|
|
||
| export async function getCaptions(audioPath: string) { | ||
| const inputPath = path.isAbsolute(audioPath) | ||
| ? audioPath | ||
| : path.join(process.cwd(), audioPath); | ||
|
|
||
| export async function getCaptions() { | ||
| const whisperCppOutput = await transcribe({ | ||
| inputPath: path.join(process.cwd(), "public/audios/audio_16k.wav"), | ||
| inputPath, | ||
| whisperPath: path.join(process.cwd(), "whisper.cpp"), | ||
| whisperCppVersion: "1.5.5", | ||
| model: "medium.en", | ||
| tokenLevelTimestamps: true, | ||
| }); | ||
|
Comment on lines
+7
to
18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling for transcription. The 🛡️ Proposed fix with error handling export async function getCaptions(audioPath: string) {
const inputPath = path.isAbsolute(audioPath)
? audioPath
: path.join(process.cwd(), audioPath);
+ if (!fs.existsSync(inputPath)) {
+ throw new Error("Audio file not found");
+ }
+
+ let whisperCppOutput;
+ try {
- const whisperCppOutput = await transcribe({
+ whisperCppOutput = await transcribe({
inputPath,
whisperPath: path.join(process.cwd(), "whisper.cpp"),
whisperCppVersion: "1.5.5",
model: "medium.en",
tokenLevelTimestamps: true,
});
+ } catch (error) {
+ console.error("Whisper transcription failed:", error);
+ throw new Error("Failed to transcribe audio");
+ }🤖 Prompt for AI Agents |
||
|
|
||
| const { captions } = toCaptions({ | ||
| whisperCppOutput, | ||
| }); | ||
| const { captions } = toCaptions({ whisperCppOutput }); | ||
|
|
||
| const fileName = `captions-${Date.now()}.json`; | ||
|
|
||
| const captionsDir = path.join(process.cwd(), "remotion/captions"); | ||
| if (!fs.existsSync(captionsDir)) { | ||
| fs.mkdirSync(captionsDir, { recursive: true }); | ||
| } | ||
|
|
||
| const captionsPath = path.join(captionsDir, fileName); | ||
|
|
||
| fs.writeFileSync(captionsPath, JSON.stringify(captions, null, 2)); | ||
|
|
||
| return captions; | ||
| } | ||
|
|
||
| return { | ||
| captionsPath: `remotion/captions/${fileName}`, | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: subhraneel2005/vidgen
Length of output: 607
🏁 Script executed:
Repository: subhraneel2005/vidgen
Length of output: 47
🏁 Script executed:
Repository: subhraneel2005/vidgen
Length of output: 629
🏁 Script executed:
Repository: subhraneel2005/vidgen
Length of output: 93
🏁 Script executed:
Repository: subhraneel2005/vidgen
Length of output: 530
🌐 Web query:
Node.js execFile default maxBuffer size child_process💡 Result:
In modern Node.js,
child_process.execFile()’soptions.maxBufferdefault is1024 * 1024bytes (1 MiB), and it applies to each ofstdoutandstderr(if either exceeds the limit, the child is terminated and output is truncated). (nodejs.org)Historically (e.g., Node 0.11 / 8.x), the default was
200 * 1024bytes (200 KiB). (r2.nodejs.org)Citations:
🌐 Web query:
ffmpeg stderr output size progress logs buffer overflow💡 Result:
When you run ffmpeg with
stderrcaptured (e.g., viasubprocess.PIPE) and you don’t continuously read it, the OS pipe buffer can fill up and ffmpeg will block/hang while trying to write more log/progress lines—this is often mistaken for a “buffer overflow.”[2]Ways to fix/avoid it:
stderrinherit the parent terminal (no pipe), or redirect it to a file.[2]stderr, read it in a dedicated thread/async loop, or merge it intostdoutand read one stream.stderr→stdout(e.g.,2>&1) if you want to pipe/parse it.[3]-nostats(since-statsis on by default).[1]-loglevel warning(orerror). (This also reduces what gets written tostderr.)-stats_period 1(or higher) to emit less frequently.[1]-progress <url>outputs periodickey=valuelines designed for parsing, and the update rate is controlled by-stats_period.[1]pipe:1/ stdout) while keeping logs minimal.[1]If you share your exact command and how you’re capturing
stderr(language + snippet), I can suggest a robust read/drain pattern for that runtime.🏁 Script executed:
Repository: subhraneel2005/vidgen
Length of output: 915
🏁 Script executed:
Repository: subhraneel2005/vidgen
Length of output: 47
Increase
maxBufferto prevent ffmpeg stderr from overflowingexecFile's 1MB default.ffmpeg writes progress logs to stderr by default; for longer audio files, this can exceed the buffer limit and silently abort conversions. Increase
maxBufferto 10MB (or higher depending on typical audio length), or alternatively add the-nostatsflag to suppress progress output.💡 Suggested fix (increase maxBuffer)
export async function mp3ToWav(mp3Path: string, wavPath: string) { - await execFileAsync("ffmpeg", [ + await execFileAsync("ffmpeg", [ "-y", "-i", mp3Path, "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", wavPath, - ]); + ], { maxBuffer: 10 * 1024 * 1024 }); }📝 Committable suggestion
🤖 Prompt for AI Agents