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

πŸ“Έ Π€ΠΎΡ‚ΠΎ-алхимия: ΠΊΠ°ΠΊ ΠΌΡ‹ ΠΈΠ·Π±Π°Π²ΠΈΠ»ΠΈ Ρ„ΠΎΡ‚ΠΎΡΡ‚ΡƒΠ΄ΠΈΡŽ ΠΎΡ‚ Ρ€ΡƒΡ‚ΠΈΠ½Ρ‹

ΠšΠ»ΠΈΠ΅Π½Ρ‚: фотостудия, которая спСциализируСтся Π½Π° свадСбной ΠΈ сСмСйной Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΈ с производством ΠΏΡ€Π΅ΠΌΠΈΡƒΠΌ-Ρ„ΠΎΡ‚ΠΎΠ°Π»ΡŒΠ±ΠΎΠΌΠΎΠ² ΠΈΠ· 200+ снимков.

Π—Π°Π΄Π°Ρ‡Π°: Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΠΎΠ½ΠΎΡ‚ΠΎΠ½Π½ΡƒΡŽ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ сотСн Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΉ, сохраняя Π΅Π΄ΠΈΠ½Ρ‹ΠΉ ΡΡ‚ΠΈΠ»ΡŒ ΠΈ качСство Π²Ρ‹Ρ…ΠΎΠ΄Π½Ρ‹Ρ… ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»ΠΎΠ².

Π§Ρ‚ΠΎ ΠΌΡ‹ сдСлали

Наша миссия β€” ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ скрипт, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΡƒΠΌΠ΅Π΅Ρ‚:

  • ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ RAW-Ρ„Π°ΠΉΠ»Ρ‹ Π² ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ JPEG;
  • ΠΊΠ°Π΄Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΈ ΠΏΠΎ Π·Π°Π΄Π°Π½Π½Ρ‹ΠΌ пропорциям;
  • Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ элСгантныС водяныС Π·Π½Π°ΠΊΠΈ с Π΄Π°Ρ‚ΠΎΠΉ ΠΈ индСксациСй;
  • Π΄Π΅Π»Π°Ρ‚ΡŒ это Π² автоматичСском Ρ€Π΅ΠΆΠΈΠΌΠ΅ Π±Π΅Π· участия Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„Π°.

Π Π°Π±ΠΎΡ‡ΠΈΠΉ ΠΏΡ€ΠΎΡ‚ΠΎΡ‚ΠΈΠΏ собрали Π·Π° 2 дня. Π‘ΠΊΡ€ΠΈΠΏΡ‚ Π½Π° TypeScript + Node.js.

Π‘Ρ‚Π΅ΠΊ

  • TypeScript β€” для типобСзопасности ΠΈ удобства Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ
  • Node.js β€” основа прилоТСния
  • fs-extra β€” Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½Π½Ρ‹ΠΉ API для Ρ€Π°Π±ΠΎΡ‚Ρ‹ с Ρ„Π°ΠΉΠ»ΠΎΠ²ΠΎΠΉ систСмой
  • canvas β€” для манипуляции изобраТСниями
  • exif β€” ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΉ
  • sips β€” macOS-ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Π° для быстрых ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ с изобраТСниями

1. ΠžΡ€Π³Π°Π½ΠΈΠ·Π°Ρ†ΠΈΡ процСсса ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ

Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ, Ρ€Π΅Π°Π»ΠΈΠ·ΡƒΡŽΡ‰ΡƒΡŽ ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ шагов ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ:

// Основной процСсс ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΉ
export const preparePhotos = async (fromDirPath: string, toDirPath: string) => {
  // Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΡŽ для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Π°Π½Π½Ρ‹Ρ… Ρ„Π°ΠΉΠ»ΠΎΠ²
  await ensureDir(toDirPath);

  // Π¨Π°Π³ 1: ΠšΠΎΠ½Π²Π΅Ρ€Ρ‚Π°Ρ†ΠΈΡ Π² JPEG
  await toJpg(fromDirPath, toDirPath);
  console.log("ΠšΠΎΠ½Π²Π΅Ρ€Ρ‚Π°Ρ†ΠΈΡ Π² JPEG Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½Π°");

  // Π¨Π°Π³ 2: ΠšΠ°Π΄Ρ€ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ
  const croppedPath = path.join(toDirPath, "cropped");
  await ensureDir(croppedPath);
  await crop(toDirPath, croppedPath);
  console.log("ΠšΠ°Π΄Ρ€ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΎ");

  // Π¨Π°Π³ 3: Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ водяных Π·Π½Π°ΠΊΠΎΠ²
  const watermarkedPath = path.join(croppedPath, "watermarked");
  await ensureDir(watermarkedPath);
  await addWatermark(croppedPath, watermarkedPath);
  console.log("ВодяныС Π·Π½Π°ΠΊΠΈ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹");
};

2. ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΡ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΎΠ² ΠΈ унификация

Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΡΠΈΡΡ‚Π΅ΠΌΠ½ΡƒΡŽ ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Ρƒ для быстрой ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚Π°Ρ†ΠΈΠΈ всСх Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΎΠ² Π² JPEG:

// ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ² Π² исходной Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ
fs.readdir(fromDirPath, (err, files) => {
  // ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΊΠ° Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ для Π²Ρ‹Ρ…ΠΎΠ΄Π½Ρ‹Ρ… Ρ„Π°ΠΉΠ»ΠΎΠ²
  fs.mkdirSync(toDirPath, { recursive: true });

  // ΠŸΠ°Ρ€Π°Π»Π»Π΅Π»ΡŒΠ½Π°Ρ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° всСх Ρ„Π°ΠΉΠ»ΠΎΠ²
  const convertPromises = files.map((file) => {
    // Π€ΠΎΡ€ΠΌΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΡƒΡ‚Π΅ΠΉ ΠΊ исходнику ΠΈ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Ρƒ
    const filePath = path.join(fromDirPath, file);
    const outputFilePath = path.join(
      toDirPath,
      path.parse(file).name + `.${JPEG_EXTENSION}`
    );

    // Асинхронный запуск ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚Π°Ρ†ΠΈΠΈ
    return new Promise((resolve, reject) => {
      exec(`sips -s format ${JPEG_EXTENSION} "${filePath}" --out "${outputFilePath}"`, 
        (error) => {
          if (error) reject(error);
          else resolve("Π€Π°ΠΉΠ» ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½");
        });
    });
  });

3. Π˜Π½Ρ‚Π΅Π»Π»Π΅ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ΅ ΠΊΠ°Π΄Ρ€ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΠΎΠ΄ Π·Π°Π΄Π°Π½Π½Ρ‹Π΅ ΠΏΡ€ΠΎΠΏΠΎΡ€Ρ†ΠΈΠΈ

ΠžΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅ΠΌ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ с ΡƒΡ‡Π΅Ρ‚ΠΎΠΌ Π΅Π³ΠΎ ΠΎΡ€ΠΈΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ ΠΈ Ρ‚Ρ€Π΅Π±ΡƒΠ΅ΠΌΡ‹Ρ… ΠΏΡ€ΠΎΠΏΠΎΡ€Ρ†ΠΈΠΉ:

// Настройка Ρ†Π΅Π»Π΅Π²ΠΎΠ³ΠΎ ΡΠΎΠΎΡ‚Π½ΠΎΡˆΠ΅Π½ΠΈΡ сторон
const aspectRatio = { x: 10, y: 15 };

// ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ списка Ρ„Π°ΠΉΠ»ΠΎΠ² для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ
const files = fs.readdirSync(fromDirPath);

// ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Ρ„Π°ΠΉΠ»ΠΎΠ² ΠΏΠΎ ΠΎΠ΄Π½ΠΎΠΌΡƒ
files.forEach((file, index) => {
  // ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ€Π°Π·ΠΌΠ΅Ρ€ΠΎΠ² исходного изобраТСния
  const imgWidth = parseInt(
    execSync(`sips -g pixelWidth "${filePath}"`)
      .toString()
      .split(":")[1]
      .trim()
  );

  // АналогичноС ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ высоты
  const imgHeight = /* ΠΊΠΎΠ΄ для получСния высоты */;

  // Адаптация ΡΠΎΠΎΡ‚Π½ΠΎΡˆΠ΅Π½ΠΈΡ ΠΏΠΎΠ΄ ΠΎΡ€ΠΈΠ΅Π½Ρ‚Π°Ρ†ΠΈΡŽ снимка
  let aspectRatioX = aspectRatio.x;
  let aspectRatioY = aspectRatio.y;

  if (imgWidth > imgHeight) {
    // Π˜Π½Π²Π΅Ρ€ΡΠΈΡ ΡΠΎΠΎΡ‚Π½ΠΎΡˆΠ΅Π½ΠΈΡ для Π³ΠΎΡ€ΠΈΠ·ΠΎΠ½Ρ‚Π°Π»ΡŒΠ½Ρ‹Ρ… Ρ„ΠΎΡ‚ΠΎ
    [aspectRatioX, aspectRatioY] = [aspectRatioY, aspectRatioX];
  }

  // ΠšΠΎΠΏΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»Π° для ΠΏΠΎΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ
  fs.copyFileSync(filePath, destFilePath);

  // ΠŸΡ€ΠΈΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ кадрирования с Π½ΠΎΠ²Ρ‹ΠΌΠΈ Ρ€Π°Π·ΠΌΠ΅Ρ€Π°ΠΌΠΈ
  execSync(`sips --cropToHeightWidth ${newHeight} ${newWidth} "${destFilePath}"`);

  console.log(`Π€Π°ΠΉΠ» ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Π°Π½: ${file}. ΠŸΡ€ΠΎΠ³Ρ€Π΅ΡΡ: ${index + 1}/${files.length}`);
});

4. ΠŸΠ΅Ρ€ΡΠΎΠ½Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡ водяных Π·Π½Π°ΠΊΠΎΠ² с ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹ΠΌΠΈ

ДобавляСм ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΠ²Π½Ρ‹Π΅ ΠΈ ΡΡ‚ΠΈΠ»ΡŒΠ½Ρ‹Π΅ водяныС Π·Π½Π°ΠΊΠΈ с ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ΠΌ Π΄Π°Ρ‚Ρ‹ ΠΈΠ· EXIF:

// ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ изобраТСния
fs.readdir(fromDirPath, async (err, files: string[]) => {
  for (const file of files) {
    // Π˜Π·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ EXIF-ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Ρ…
    new ExifImage({ image: filePath }, async (error, exifData: ExifData) => {
      // ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΊΠ° изобраТСния ΠΊ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡŽ
      const image = await loadImage(filePath);
      const canvas = createCanvas(image.width, image.height);
      const ctx = canvas.getContext("2d");

      // Π Π°Π·ΠΌΠ΅Ρ‰Π΅Π½ΠΈΠ΅ изобраТСния Π½Π° холстС
      ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

      // Π˜Π·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ Π΄Π°Ρ‚Ρ‹ создания ΠΈΠ· ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Ρ…
      let dateCreated;
      if (exifData?.exif?.CreateDate) {
        dateCreated = exifData.exif.CreateDate
          .replace(":", "-")
          .replace(":", "-");
      }
      
      // ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΊΠ° ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ для водяного Π·Π½Π°ΠΊΠ°
      const photoCaption = path.parse(file).name;

      // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° наличия ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‰ΠΈΡ… ΠΌΠ΅Ρ‚ΠΎΠΊ Π² ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π°
      const skipWatermark = photoCaption.includes("nowm");
      const partialName = photoCaption.includes("name") 
        ? photoCaption.split("name")[0] 
        : photoCaption;

      // Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ водяного Π·Π½Π°ΠΊΠ° ΠΏΡ€ΠΈ нСобходимости
      if (partialName && !skipWatermark) {
        writeText(
          ctx,
          `${partialName}${
            dateCreated ? ` β€’ ${dateCreated}` : ""
          }`,
          35, // отступ ΠΏΠΎ Π³ΠΎΡ€ΠΈΠ·ΠΎΠ½Ρ‚Π°Π»ΠΈ
          75  // позиция ΠΏΠΎ Π²Π΅Ρ€Ρ‚ΠΈΠΊΠ°Π»ΠΈ
        );
      }

      // Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π° Π² Ρ„Π°ΠΉΠ»
      const out = fs.createWriteStream(destFilePath);
      const stream = canvas.createJPEGStream({
        quality: 0.95,
        progressive: true
      });

      stream.pipe(out);

      out.on("finish", () => {
        console.log(`ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Π°Π½ Ρ„Π°ΠΉΠ»: ${file}`);
      });
    });
  }
});

// Ѐункция для отрисовки тСкстового водяного Π·Π½Π°ΠΊΠ°
function writeText(ctx, text, x, y) {
  // ΠžΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ Ρ€Π°Π·ΠΌΠ΅Ρ€ΠΎΠ² тСкста
  const textWidth = ctx.measureText(text).width;

  // Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Ρ„ΠΎΠ½Π° для тСкста
  ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
  ctx.fillRect(0, y - 75, textWidth + 70, 100);

  // Настройка стиля ΠΈ отрисовка тСкста
  ctx.font = "24px Helvetica";
  ctx.fillStyle = "white";
  ctx.strokeStyle = "black";
  ctx.lineWidth = 1;

  ctx.strokeText(text, x, y);
  ctx.fillText(text, x, y);
}

βœ… Π§Ρ‚ΠΎ Π² ΠΈΡ‚ΠΎΠ³Π΅ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ» ΠΊΠ»ΠΈΠ΅Π½Ρ‚

  • Π‘ΠΎΠΊΡ€Π°Ρ‰Π΅Π½ΠΈΠ΅ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ с 8 часов Π΄ΠΎ 15 ΠΌΠΈΠ½ΡƒΡ‚ Π½Π° Ρ„ΠΎΡ‚ΠΎΡΠ΅ΡΡΠΈΡŽ
  • Π£Π½ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ ΡΡ‚ΠΈΠ»ΡŒ всСх Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΉ Π² ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π΅
  • АвтоматичСская брСндированная ΠΌΠ°Ρ€ΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ
  • ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹, Π³ΠΎΡ‚ΠΎΠ²Ρ‹Π΅ для ΠΏΠ΅Ρ‡Π°Ρ‚ΠΈ ΠΈΠ»ΠΈ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ
  • Экономия ΠΎΠΊΠΎΠ»ΠΎ 40 тысяч Ρ€ΡƒΠ±Π»Π΅ΠΉ СТСмСсячно Π½Π° постобработкС

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

  • Π’Π½Π΅Π΄Ρ€ΠΈΡ‚ΡŒ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΡ‡Π΅ΡΠΊΡƒΡŽ Ρ†Π²Π΅Ρ‚ΠΎΠΊΠΎΡ€Ρ€Π΅ΠΊΡ†ΠΈΡŽ ΠΈ Ρ‚ΠΎΠ½Π°Π»ΡŒΠ½ΡƒΡŽ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ
  • Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ распознаваниС ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ² для ΡƒΠΌΠ½ΠΎΠ³ΠΎ кадрирования
  • Π Π°Π·Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ графичСский интСрфСйс Π½Π° Electron для Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΎΠ²
  • Π‘ΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒ Π½Π°Π±ΠΎΡ€Ρ‹ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ Ρ€Π°Π·Π½Ρ‹Ρ… Ρ€Π°Π·ΠΌΠ΅Ρ€ΠΎΠ² для Ρ€Π°Π·Π½Ρ‹Ρ… ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌ
  • Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с сСрвисами ΠΏΠ΅Ρ‡Π°Ρ‚ΠΈ ΠΈ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ альбомов

Π£ вас Π΅ΡΡ‚ΡŒ творчСскиС процСссы, Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‰ΠΈΠ΅ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΠ·Π°Ρ†ΠΈΠΈ?

ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ Π½Π°ΠΌ β€” ΠΏΡ€Π΅Π²Ρ€Π°Ρ‚ΠΈΠΌ Ρ€ΡƒΡ‚ΠΈΠ½Π½Ρ‹Π΅ Π·Π°Π΄Π°Ρ‡ΠΈ Π² ΠΎΠ΄ΠΈΠ½ ΠΊΠ»ΠΈΠΊ, освобоТдая врСмя для Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π²Π°ΠΆΠ½ΠΎΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹.