Π ΡƒΠ±Ρ€ΠΈΠΊΠΈ
Код-Π½ΠΎΡƒΡ…Π°Ρƒ

🎬 АвтоматичСский ΠΌΠΎΠ½Ρ‚Π°ΠΆ сториз Π² Π˜Π½ΡΡ‚Π°Π³Ρ€Π°ΠΌ: ΠΊΠ°ΠΊ скрипт Π·Π° 1 дСнь Π·Π°ΠΌΠ΅Π½ΠΈΠ» Π²ΠΈΠ΄Π΅ΠΎ-ΠΌΠΎΠ½Ρ‚Π°ΠΆΡ‘Ρ€Π°

ΠšΠ»ΠΈΠ΅Π½Ρ‚: нСбольшой Π±Ρ€Π΅Π½Π΄ ΠΎΠ΄Π΅ΠΆΠ΄Ρ‹. РСгулярно Π²Ρ‹ΠΊΠ»Π°Π΄Ρ‹Π²Π°Π΅Ρ‚ сториз ΠΈΠ· Π²ΠΈΠ΄Π΅ΠΎΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°: Π·Π°ΠΊΡƒΠ»ΠΈΡΡŒΠ΅, Π½ΠΎΠ²Ρ‹Π΅ ΠΌΠΎΠ΄Π΅Π»ΠΈ, сборки Π·Π°ΠΊΠ°Π·ΠΎΠ².

Π—Π°Π΄Π°Ρ‡Π°: ΡƒΡΠΊΠΎΡ€ΠΈΡ‚ΡŒ Ρ€ΡƒΡ‚ΠΈΠ½Π½Ρ‹ΠΉ ΠΌΠΎΠ½Ρ‚Π°ΠΆ сториз Π² Π˜Π½ΡΡ‚Π°Π³Ρ€Π°ΠΌ ΠΏΠΎΠ΄ ΠΌΡƒΠ·Ρ‹ΠΊΡƒ.

πŸ’‘ Π§Ρ‚ΠΎ ΠΌΡ‹ сдСлали

Наша Ρ†Π΅Π»ΡŒ β€” ΡΠΎΠ±Ρ€Π°Ρ‚ΡŒ скрипт, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ:

  • ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Π²ΠΈΠ΄Π΅ΠΎ ΠΈ ΠΌΡƒΠ·Ρ‹ΠΊΡƒ Π½Π° Π²Ρ…ΠΎΠ΄Π΅;
  • Π½Π°Ρ€Π΅Π·Π°Π΅Ρ‚ ΠΊΠ»ΠΈΠΏΡ‹ ΠΏΠΎ Π±ΠΈΡ‚Π°ΠΌ ΠΌΡƒΠ·Ρ‹ΠΊΠΈ;
  • склСиваСт Π² ΠΎΠ΄Π½ΠΎ Π²ΠΈΠ΄Π΅ΠΎ;
  • экспортируСт Π³ΠΎΡ‚ΠΎΠ²ΡƒΡŽ сториз.

Π Π°Π±ΠΎΡ‡ΠΈΠΉ ΠΏΡ€ΠΎΡ‚ΠΎΡ‚ΠΈΠΏ ΠΌΡ‹ сдСлали Π·Π° дСнь. Π‘ΠΊΡ€ΠΈΠΏΡ‚ Π½Π° Node.js + ffmpeg.

🧱 Π‘Ρ‚Π΅ΠΊ

  • Node.js β€” основа
  • ffmpeg β€” ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π²ΠΈΠ΄Π΅ΠΎ ΠΈ Π°ΡƒΠ΄ΠΈΠΎ
  • fs-extra β€” Ρ€Π°Π±ΠΎΡ‚Π° с Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ
  • fluent-ffmpeg β€” удобная ΠΎΠ±Ρ‘Ρ€Ρ‚ΠΊΠ° для ffmpeg
  • child_process β€” для запуска shell-ΠΊΠΎΠΌΠ°Π½Π΄

1. Π—Π°Π΄Π°Ρ‘ΠΌ Π±Π°Π·ΠΎΠ²Ρ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ для Π½Π°Ρ€Π΅Π·ΠΊΠΈ

🎯 Π­Ρ‚ΠΎ Π½ΡƒΠΆΠ½ΠΎ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ, Π½Π° ΠΊΠ°ΠΊΠΈΠ΅ Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ ΠΎΡ‚Ρ€Π΅Π·ΠΊΠΈ Π±ΡƒΠ΄Π΅ΠΌ Π½Π°Ρ€Π΅Π·Π°Ρ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎ β€” Π² нашСм случаС ΠΏΠΎ Π±ΠΈΡ‚Π°ΠΌ ΠΌΡƒΠ·Ρ‹ΠΊΠΈ.

const bpm = 85; // Π—Π°Π΄Π°Ρ‘ΠΌ Ρ‚Π΅ΠΌΠΏ ΠΌΡƒΠ·Ρ‹ΠΊΠΈ (BPM)
const beatsPerSegment = 2; // ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π±ΠΈΡ‚ΠΎΠ² Π½Π° ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ сСгмСнт
const beatsPerVideo = 4; // ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π±ΠΈΡ‚ΠΎΠ² Π½Π° ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ Π²ΠΈΠ΄Π΅ΠΎ

// РасчСт Π΄Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ ΠΎΠ΄Π½ΠΎΠ³ΠΎ сСгмСнта
const secondsPerBeat = 60 / bpm;
const secondsPerSegment = secondsPerBeat * beatsPerSegment;

console.log(`Π”Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ ΠΎΠ΄Π½ΠΎΠ³ΠΎ сСгмСнта: ${secondsPerSegment} сСкунд`);

2. ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π΄Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ Π°ΡƒΠ΄ΠΈΠΎΡ‚Ρ€Π΅ΠΊΠ°

🎯 Π§Ρ‚ΠΎΠ±Ρ‹ Π·Π½Π°Ρ‚ΡŒ, сколько Π±ΠΈΡ‚ΠΎΠ² Π²Π»Π΅Π·Π°Π΅Ρ‚ Π² Ρ‚Ρ€Π΅ΠΊ β€” ΠΈ соотвСтствСнно, сколько Π½Π°Ρ€Π΅Π·ΠΎΠΊ Π½ΡƒΠΆΠ½ΠΎ.

// Π”Π»ΠΈΠ½Π° ΠΌΡƒΠ·Ρ‹ΠΊΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ Ρ‚Ρ€Π΅ΠΊΠ°
const trackDuration = execSync(
  `ffmpeg -i ${musicFile} 2>&1 | grep "Duration"`
)
  .toString()
  .match(/Duration: (\d+):(\d+):(\d+\.\d+)/);

if (!trackDuration) {
  console.error("НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ Π΄Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ Ρ‚Ρ€Π΅ΠΊΠ°.");
  process.exit(1);
}

const [_, trackHours, trackMinutes, trackSeconds] = trackDuration.map(Number);
const totalTrackSeconds =
  trackHours * 3600 + trackMinutes * 60 + trackSeconds;

console.log(`ΠžΠ±Ρ‰Π°Ρ Π΄Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ Ρ‚Ρ€Π΅ΠΊΠ° Π² сСкундах: ${totalTrackSeconds}`);

3. ВычисляСм количСство Π½ΡƒΠΆΠ½Ρ‹Ρ… Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ΠΎΠ²

🎯 ΠŸΡ€ΠΎΡΡ‚ΠΎ количСство beat-ΠΈΠ½Ρ‚Π΅Ρ€Π²Π°Π»ΠΎΠ², Π½Π° ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΡ€Π΅Π·Π°Ρ‚ΡŒ вСсь Ρ‚Ρ€Π΅ΠΊ.

// РасчСт количСства сСгмСнтов
const segmentCount =
  beatsPerVideo || Math.floor(totalTrackSeconds / secondsPerSegment);

console.log(`ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ сСгмСнтов: ${segmentCount}`);

4. ΠŸΠΎΠ΄Π³ΠΎΡ‚Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ Ρ„ΡƒΡ‚Π°ΠΆΠΈ для Π½Π°Ρ€Π΅Π·ΠΊΠΈ

🎯 Находим подходящиС ΠΏΠΎ Π΄Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ Π²ΠΈΠ΄Π΅ΠΎ для создания сСгмСнтов.

// ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ списка всСх Ρ„ΡƒΡ‚Π°ΠΆΠ΅ΠΉ ΠΈ ΠΈΡ… сортировка
let footageFiles = fs
  .readdirSync(footageFolder)
  .filter((file) => file.endsWith(".mp4"))
  .sort();

// Π€ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡ Π²ΠΈΠ΄Π΅ΠΎ ΠΏΠΎ Π΄Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ
footageFiles = await Promise.all(
  footageFiles.map(async (file) => {
    const filePath = path.join(footageFolder, file);
    const duration = await getVideoDuration(filePath);
    return duration >= secondsPerSegment ? filePath : null;
  })
);
footageFiles = footageFiles.filter((file) => file !== null);

console.log(`НайдСно подходящих Ρ„ΡƒΡ‚Π°ΠΆΠ΅ΠΉ: ${footageFiles.length}`);

5. НарСзаСм ΠΊΠ»ΠΈΠΏΡ‹ ΠΈΠ· исходных Π²ΠΈΠ΄Π΅ΠΎ

🎯 Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Ρ‡Π΅Ρ€Π΅Π΄ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠ»ΠΈΠΏΠΎΠ², выбирая ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ ΠΈΠ· доступных Ρ„ΡƒΡ‚Π°ΠΆΠ΅ΠΉ.

// Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… сСгмСнтов ΠΈΠ· Ρ„ΡƒΡ‚Π°ΠΆΠ΅ΠΉ
const segments = [];
for (let i = 0; i < segmentCount; i++) {
  const footageIndex = i % footageFiles.length;
  const footageFile = footageFiles[footageIndex];
  const segmentFile = path.join(outputFolder, `segment${i}.mp4`);

  await new Promise((resolve, reject) => {
    console.log(`Начало ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ сСгмСнта ${i} ΠΈΠ· Ρ„Π°ΠΉΠ»Π° ${footageFile}...`);
    ffmpeg()
      .input(footageFile)
      .output(segmentFile)
      .outputOptions([
        `-ss 0`,
        `-t ${secondsPerSegment}`,
        `-r 30`, // установка Ρ„Ρ€Π΅ΠΉΠΌΡ€Π΅ΠΉΡ‚Π° для прСдотвращСния зависаний
        `-vcodec libx264`, // установка Π²ΠΈΠ΄Π΅ΠΎ ΠΊΠΎΠ΄Π΅ΠΊΠ° для ΡƒΠ½ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ
        `-an`, // ΠΎΡ‚ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Π°ΡƒΠ΄ΠΈΠΎ
        `-fflags +genpts`, // гСнСрация Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… ΠΌΠ΅Ρ‚ΠΎΠΊ
      ])
      .on("progress", (progress) => {
        console.log(`Π‘Π΅Π³ΠΌΠ΅Π½Ρ‚ ${i} прогрСсс: ${progress.percent}%`);
      })
      .on("end", () => {
        console.log(`Π‘Π΅Π³ΠΌΠ΅Π½Ρ‚ ${i} Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½`);
        segments.push(segmentFile);
        resolve();
      })
      .on("error", (err) => {
        console.error(`Ошибка ΠΏΡ€ΠΈ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅ сСгмСнта ${i}: ${err}`);
        reject(err);
      })
      .run();
  });
}

6. Π‘ΠΊΠ»Π΅ΠΈΠ²Π°Π΅ΠΌ Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚Ρ‹ Π² ΠΎΠ΄ΠΈΠ½ Ρ„Π°ΠΉΠ»

🎯 FFmpeg позволяСт ΡΠΊΠ»Π΅ΠΈΡ‚ΡŒ ΠΌΠ½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»ΠΎΠ², Ссли ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ ΠΈΠΌ список.

// Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»Π° со списком сСгмСнтов
const concatList = segments
  .map((file) => `file '${path.resolve(file)}'`)
  .join("\n");
fs.writeFileSync(path.join(outputFolder, "concat_list.txt"), concatList);

// ОбъСдинСниС всСх сСгмСнтов Π² ΠΎΠ΄Π½ΠΎ Π²ΠΈΠ΄Π΅ΠΎ Π±Π΅Π· Π°ΡƒΠ΄ΠΈΠΎ
await new Promise((resolve, reject) => {
  const concatListPath = path.join(outputFolder, "concat_list.txt");

  ffmpeg()
    .input(concatListPath)
    .inputOptions(["-f concat", "-safe 0"])
    .outputOptions([
      "-c:v libx264", // ΠŸΠ΅Ρ€Π΅ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² ΠΎΠ΄ΠΈΠ½ ΠΊΠΎΠ΄Π΅ΠΊ для консистСнции
      "-pix_fmt yuv420p", // ΠžΠ±Π΅ΡΠΏΠ΅Ρ‡Π΅Π½ΠΈΠ΅ совмСстимости с Π±ΠΎΠ»ΡŒΡˆΠΈΠ½ΡΡ‚Π²ΠΎΠΌ ΠΏΠ»Π΅Π΅Ρ€ΠΎΠ²
      "-fflags +genpts", // гСнСрация Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… ΠΌΠ΅Ρ‚ΠΎΠΊ
    ])
    .output(path.join(outputFolder, "output_without_audio.mp4"))
    .on("end", () => {
      console.log("Π’ΠΈΠ΄Π΅ΠΎ Π±Π΅Π· Π°ΡƒΠ΄ΠΈΠΎ создано: output_without_audio.mp4");
      resolve();
    })
    .run();
});

7. ДобавляСм ΠΌΡƒΠ·Ρ‹ΠΊΡƒ ΠΊ Π³ΠΎΡ‚ΠΎΠ²ΠΎΠΌΡƒ Π²ΠΈΠ΄Π΅ΠΎ

🎯 На Π²Ρ‹Ρ…ΠΎΠ΄Π΅ Ρƒ нас ΡƒΠΆΠ΅ склССнноС Π²ΠΈΠ΄Π΅ΠΎ, ΠΎΡΡ‚Π°Π»ΠΎΡΡŒ Π½Π°Π»ΠΎΠΆΠΈΡ‚ΡŒ ΠΌΡƒΠ·Ρ‹ΠΊΡƒ ΠΈ ΠΎΠ±Ρ€Π΅Π·Π°Ρ‚ΡŒ ΠΏΠΎ ΠΊΠΎΡ€ΠΎΡ‚ΠΊΠΎΠΌΡƒ Ρ‚Ρ€Π΅ΠΊΡƒ.

// Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π°ΡƒΠ΄ΠΈΠΎ ΠΊ Π²ΠΈΠ΄Π΅ΠΎ ΠΈ ΠΎΠ±Ρ€Π΅Π·ΠΊΠ° Π²ΠΈΠ΄Π΅ΠΎ Π΄ΠΎ Π΄Π»ΠΈΠ½Ρ‹ Π°ΡƒΠ΄ΠΈΠΎ
await new Promise((resolve, reject) => {
  console.log("Начало добавлСния Π°ΡƒΠ΄ΠΈΠΎ...");
  ffmpeg()
    .input(path.join(outputFolder, "output_without_audio.mp4"))
    .input(musicFile)
    .outputOptions([
      "-c:v copy",
      "-c:a aac",
      "-strict experimental",
      "-shortest", // Π’Ρ‹Ρ€Π΅Π·Π°Π΅Ρ‚ Π²ΠΈΠ΄Π΅ΠΎ Π΄ΠΎ Π΄Π»ΠΈΠ½Ρ‹ Π°ΡƒΠ΄ΠΈΠΎ
    ])
    .output(path.join(outputFolder, "final_video.mp4"))
    .on("progress", (progress) => {
      console.log(`Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π°ΡƒΠ΄ΠΈΠΎ прогрСсс: ${progress.percent}%`);
    })
    .on("end", () => {
      console.log("Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π°ΡƒΠ΄ΠΈΠΎ Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΎ");
      resolve();
    })
    .run();
});

console.log("ЀинальноС Π²ΠΈΠ΄Π΅ΠΎ с Π°ΡƒΠ΄ΠΈΠΎ создано: final_video.mp4");

βœ… Π§Ρ‚ΠΎ Π² ΠΈΡ‚ΠΎΠ³Π΅

  • 1 ΠΊΠ»ΠΈΠΊ β†’ Π΄ΠΎ 85 Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ΠΎΠ² β†’ готовая сториз
  • 2-3 ΠΌΠΈΠ½ΡƒΡ‚Ρ‹ Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ вмСсто часов Ρ€ΡƒΡ‡Π½ΠΎΠ³ΠΎ ΠΌΠΎΠ½Ρ‚Π°ΠΆΠ°
  • ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π²Ρ‹Ρ…ΠΎΠ΄ΠΈΡ‚ рСгулярно, Π±Π΅Π· ΠΏΡ€ΠΎΠ²ΠΎΠ»ΠΎΡ‡Π΅ΠΊ
  • ΠšΠ»ΠΈΠ΅Π½Ρ‚ Ρ‚Ρ€Π°Ρ‚ΠΈΡ‚ 0 Ρ€ΡƒΠ±Π»Π΅ΠΉ Π½Π° ΠΌΠΎΠ½Ρ‚Π°ΠΆ

🧠 Π§Ρ‚ΠΎ ΠΌΠΎΠΆΠ½ΠΎ ΡƒΠ»ΡƒΡ‡ΡˆΠΈΡ‚ΡŒ

  • АвтоматичСскоС ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ BPM вмСсто Ρ€ΡƒΡ‡Π½ΠΎΠ³ΠΎ Π²Π²ΠΎΠ΄Π°
  • РаспознаваниС «ΠΈΠ½Ρ‚СрСсных» ΠΌΠΎΠΌΠ΅Π½Ρ‚ΠΎΠ² ΠΏΠΎ Π·Π²ΡƒΠΊΡƒ ΠΈΠ»ΠΈ двиТСнию Π² ΠΊΠ°Π΄Ρ€Π΅
  • UI Π½Π° React: Π²Ρ‹Π±Ρ€Π°Ρ‚ΡŒ ΠΏΠ°ΠΏΠΊΡƒ с ΠΊΠ»ΠΈΠΏΠ°ΠΌΠΈ, ΠΌΡƒΠ·Ρ‹ΠΊΡƒ, Π½Π°ΠΆΠ°Ρ‚ΡŒ ΠΊΠ½ΠΎΠΏΠΊΡƒ β†’ Π³ΠΎΡ‚ΠΎΠ²ΠΎ
  • Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ‡Π΅Ρ€Π΅Π· Telegram-Π±ΠΎΡ‚Π° ΠΈΠ»ΠΈ web-интСрфСйс

πŸ“© Π•ΡΡ‚ΡŒ Π·Π°Π΄Π°Ρ‡ΠΈ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΆΡ€ΡƒΡ‚ врСмя ΠΈ силы?

Π”Π°Π²Π°ΠΉΡ‚Π΅ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΠ·ΠΈΡ€ΡƒΠ΅ΠΌ. ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ Π½Π°ΠΌ β€” ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠΈΠΌ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎΠ΅ Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅.