admin管理员组

文章数量:1391925

I'm trying to burn subtitles .ass file into an .mp4 video.
When I try to do so the process finishes and renders the same video without subtitles.

I think the problem is with the font, at least that's what ChatGPT told me, but I'm not sure what to do. Please take a look the code and tell me, if I did it correctly!

The code:

import { FFmpeg } from "@ffmpeg/ffmpeg"
import { fetchFile } from "@ffmpeg/util"
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"

export async function burnSubtitles(inputVideo: string, inputSubtitles: string, session: any, videoFileName: string) {
  const ffmpeg = new FFmpeg()
  const supabase = createClientComponentClient()

  try {
    console.log("Loading FFmpeg...")
    await ffmpeg.load({
      coreURL: "/@ffmpeg/[email protected]/dist/umd/ffmpeg-core.js",
      log: true,
    })

    console.log("FFmpeg Loaded!")

    // Fetch video and subtitle files
    console.log("Fetching video and subtitle files...")
    const videoFile = await fetchFile(inputVideo)
    const subtitleFile = await fetchFile(inputSubtitles)

    console.log("Writing files to FFmpeg virtual filesystem...")
    await ffmpeg.writeFile("input.mp4", videoFile)
    await ffmpeg.writeFile("subtitles.ass", subtitleFile)

    console.log("Running FFmpeg command...")
    await ffmpeg.exec([
      "-i",
      "input.mp4",
      "-vf",
      "subtitles=subtitles.ass",
      "-c:v",
      "libx264",
      "-preset",
      "fast",
      "-crf",
      "23",
      "-c:a",
      "aac",
      "-b:a",
      "128k",
      "output.mp4",
    ])

    console.log("FFmpeg processing completed!")

    // Get the output file
    console.log("Reading output file...")
    const outputData = await ffmpeg.readFile("output.mp4")

    // Generate file paths
    const processedFile = videoFileName ? `${videoFileName.split(".")[0]}.mp4` : `${Date.now()}.mp4`
    const processedFilePath = `processed/${session.user.id}/${processedFile}`

    console.log("Uploading processed video to Supabase...")

    // Upload the processed video to Supabase
    const { error } = await supabase.storage
      .from("processed")
      .upload(processedFilePath, new Blob([outputData.buffer], { type: "video/mp4" }), { upsert: true })

    if (error) {
      throw new Error(`Failed to upload processed video: ${error.message}`)
    }

    // Get the public URL of the uploaded video
    const {
      data: { publicUrl },
    } = supabase.storage.from("processed").getPublicUrl(processedFilePath)

    console.log("Subtitled video uploaded successfully:", publicUrl)
    return {
      success: true,
      videoUrl: publicUrl,
    }
  } catch (error) {
    console.error("Error in FFmpeg processing:", error)
    return {
      success: false,
      error: "Processing failed",
    }
  } finally {
    ffmpeg.terminate()
  }
}    

The ass file:

[Script Info]
ScriptType: v4.00+
PlayResX: 1920
PlayResY: 1080
ScaledBorderAndShadow: yes

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,16,&HFFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,2,2,30,30,360,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,00:00:00.00,00:00:01.11,Default,,0,0,360,,{\fad(300,0)\t(0,350,\fscx80\fscy80)\t(350,600,\fscx100\fscy100)\shad2}THIS IS A TEST,
Dialogue: 0,00:00:01.11,00:00:02.22,Default,,0,0,360,,{\fad(300,0)\t(0,350,\fscx80\fscy80)\t(350,600,\fscx100\fscy100)\shad2}SUBTITLE

When I try to do so the process finishes, and renders the same video without subtitles.

I'm expecting the rendered video to have the subtitles.

I'm trying to burn subtitles .ass file into an .mp4 video.
When I try to do so the process finishes and renders the same video without subtitles.

I think the problem is with the font, at least that's what ChatGPT told me, but I'm not sure what to do. Please take a look the code and tell me, if I did it correctly!

The code:

import { FFmpeg } from "@ffmpeg/ffmpeg"
import { fetchFile } from "@ffmpeg/util"
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"

export async function burnSubtitles(inputVideo: string, inputSubtitles: string, session: any, videoFileName: string) {
  const ffmpeg = new FFmpeg()
  const supabase = createClientComponentClient()

  try {
    console.log("Loading FFmpeg...")
    await ffmpeg.load({
      coreURL: "https://cdn.jsdelivr/npm/@ffmpeg/[email protected]/dist/umd/ffmpeg-core.js",
      log: true,
    })

    console.log("FFmpeg Loaded!")

    // Fetch video and subtitle files
    console.log("Fetching video and subtitle files...")
    const videoFile = await fetchFile(inputVideo)
    const subtitleFile = await fetchFile(inputSubtitles)

    console.log("Writing files to FFmpeg virtual filesystem...")
    await ffmpeg.writeFile("input.mp4", videoFile)
    await ffmpeg.writeFile("subtitles.ass", subtitleFile)

    console.log("Running FFmpeg command...")
    await ffmpeg.exec([
      "-i",
      "input.mp4",
      "-vf",
      "subtitles=subtitles.ass",
      "-c:v",
      "libx264",
      "-preset",
      "fast",
      "-crf",
      "23",
      "-c:a",
      "aac",
      "-b:a",
      "128k",
      "output.mp4",
    ])

    console.log("FFmpeg processing completed!")

    // Get the output file
    console.log("Reading output file...")
    const outputData = await ffmpeg.readFile("output.mp4")

    // Generate file paths
    const processedFile = videoFileName ? `${videoFileName.split(".")[0]}.mp4` : `${Date.now()}.mp4`
    const processedFilePath = `processed/${session.user.id}/${processedFile}`

    console.log("Uploading processed video to Supabase...")

    // Upload the processed video to Supabase
    const { error } = await supabase.storage
      .from("processed")
      .upload(processedFilePath, new Blob([outputData.buffer], { type: "video/mp4" }), { upsert: true })

    if (error) {
      throw new Error(`Failed to upload processed video: ${error.message}`)
    }

    // Get the public URL of the uploaded video
    const {
      data: { publicUrl },
    } = supabase.storage.from("processed").getPublicUrl(processedFilePath)

    console.log("Subtitled video uploaded successfully:", publicUrl)
    return {
      success: true,
      videoUrl: publicUrl,
    }
  } catch (error) {
    console.error("Error in FFmpeg processing:", error)
    return {
      success: false,
      error: "Processing failed",
    }
  } finally {
    ffmpeg.terminate()
  }
}    

The ass file:

[Script Info]
ScriptType: v4.00+
PlayResX: 1920
PlayResY: 1080
ScaledBorderAndShadow: yes

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,16,&HFFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,2,2,30,30,360,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,00:00:00.00,00:00:01.11,Default,,0,0,360,,{\fad(300,0)\t(0,350,\fscx80\fscy80)\t(350,600,\fscx100\fscy100)\shad2}THIS IS A TEST,
Dialogue: 0,00:00:01.11,00:00:02.22,Default,,0,0,360,,{\fad(300,0)\t(0,350,\fscx80\fscy80)\t(350,600,\fscx100\fscy100)\shad2}SUBTITLE

When I try to do so the process finishes, and renders the same video without subtitles.

I'm expecting the rendered video to have the subtitles.

Share Improve this question asked Mar 14 at 5:54 Diyan TodorovDiyan Todorov 1
Add a comment  | 

1 Answer 1

Reset to default -1
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"
import { cookies } from "next/headers"

interface WhisperSegment {
  start: number
  end: number
  text: string
}

interface WhisperResponse {
  segments?: WhisperSegment[]
}

function formatTimeASS(seconds: number): string {
  const hours = Math.floor(seconds / 3600)
  const minutes = Math.floor((seconds % 3600) / 60)
  const secs = Math.floor(seconds % 60)
  const centiseconds = Math.floor((seconds % 1) * 100)
  return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}.${centiseconds.toString().padStart(2, "0")}`
}

function splitTextIntoChunks(text: string): string[] {
  const words = text.split(" ")
  const chunks = []
  let chunk = ""

  for (const word of words) {
    chunk += word + " "
    if (/[.!?:]$/.test(word) || chunk.split(" ").length >= 4) {
      chunks.push(chunk.trim().toUpperCase())
      chunk = ""
    }
  }

  if (chunk) chunks.push(chunk.trim().toUpperCase())
  return chunks
}

function createAssContent(segments: WhisperSegment[]): string {
  const header = `[Script Info]
ScriptType: v4.00+
PlayResX: 1920
PlayResY: 1080
ScaledBorderAndShadow: yes

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Montserrat Black,48,&HFFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,2,2,30,30,360,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`

  let dialogues = ""

  for (const segment of segments) {
    dialogues += `Dialogue: 0,${formatTimeASS(segment.start)},${formatTimeASS(segment.end)},Default,,0,0,360,,{\\fad(300,0)\\t(0,350,\\fscx80\\fscy80)\\t(350,600,\\fscx100\\fscy100)\\shad2}${segment.text.toUpperCase()}\n`
  }

  return header + dialogues.trim()
}

async function checkTextWithGPT4(segments: WhisperSegment[]): Promise<WhisperSegment[]> {
  const correctedSegments = [...segments]

  try {
    for (let i = 0; i < segments.length; i++) {
      const segment = segments[i]

      const response = await fetch("https://api.openai/v1/chat/completions", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        },
        body: JSON.stringify({
          model: "gpt-4o",
          messages: [
            {
              role: "system",
              content:
                "You are a conservative spelling and grammar checker. ONLY fix clear spelling errors and obvious grammar mistakes. DO NOT change any words that are correctly spelled, even if they seem unusual. DO NOT alter the meaning, style, or vocabulary choices. DO NOT add or remove content. If the text is already correct, return it unchanged.",
            },
            {
              role: "user",
              content: segment.text,
            },
          ],
          temperature: 0.1,
        }),
      })

      if (response.ok) {
        const data = await response.json()
        correctedSegments[i] = {
          ...segment,
          text: data.choices[0].message.content.trim() || segment.text,
        }
      }
    }

    return correctedSegments
  } catch (error) {
    return segments
  }
}

export async function POST(req: Request) {
  try {
    const supabase = createRouteHandlerClient({ cookies })
    const {
      data: { session },
    } = await supabase.auth.getSession()

    if (!session) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
    }

    const formData = await req.formData()
    const audioUrl = formData.get("audioUrl") as string
    const videoFileName = formData.get("videoFileName") as string

    if (!audioUrl) {
      return NextResponse.json({ error: "Audio URL is required" }, { status: 400 })
    }

    const audioResponse = await fetch(audioUrl)
    const audioBlob = await audioResponse.blob()

    const openAIFormData = new FormData()
    openAIFormData.append("file", audioBlob, "audio.mp3")
    openAIFormData.append("model", "whisper-1")
    openAIFormData.append("language", "bg")
    openAIFormData.append("response_format", "verbose_json")

    const openAIResponse = await fetch("https://api.openai/v1/audio/transcriptions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      },
      body: openAIFormData,
    })

    if (!openAIResponse.ok) {
      throw new Error("Failed to transcribe audio")
    }

    const jsonContent: WhisperResponse = await openAIResponse.json()

    if (!jsonContent.segments || jsonContent.segments.length === 0) {
      return NextResponse.json({ error: "No transcription segments found" }, { status: 400 })
    }

    const correctedSegments = await checkTextWithGPT4(jsonContent.segments)
    const assContent = createAssContent(correctedSegments)
    const assFileName = videoFileName ? `${videoFileName.split(".")[0]}.ass` : `${Date.now()}.ass`
    const subtitlePath = `subtitles/${session.user.id}/${assFileName}`

    await supabase.storage
      .from("extracts")
      .upload(subtitlePath, new Blob([assContent], { type: "text/plain" }), { upsert: true })

    const {
      data: { publicUrl },
    } = supabase.storage.from("extracts").getPublicUrl(subtitlePath)

    return NextResponse.json({
      success: true,
      subtitleUrl: publicUrl,
    })
  } catch (error) {
    return NextResponse.json({ error: "Processing failed" }, { status: 500 })
  }
}"```

Thats how i fixed it.

本文标签: nodejsBurn ass into mp4 using FFMPEG WASMStack Overflow