Недавно OpenAi выпустила новую модель Sora 2, которая взорвала интернет благодаря бесплатному доступу и большим лимитом на генерации. Каждый день пользователю доступна генерация 30 видеороликов длительностью по 10 секунд или 15 видеороликов по 15 секунд.

Это привело меня к мысли, что на её основе можно начать раскрутку соцсетей органическим трафиком. Но как выделиться среди других?

Меняем Watermark

Первое, что меня озадачило - это наличие во всех роликах вотермарки Sora. Её наличие отпугивает потенциальных зрителей, демонстрируя низкое качество и потоковость контента.

Пример вотермарки
Пример вотермарки

На Github уже присутствовали решения, скрывающие вотермарку, но я решил написать своё. Для начала я просто написал сравнение по картинке с поиском зоны вотермарки с её последующим перекрытием.

def detect_watermark_zone(frame, watermark_template, threshold=0.3):
    result = cv2.matchTemplate(frame, watermark_template, cv2.TM_CCOEFF_NORMED)
    _, max_val, _, max_loc = cv2.minMaxLoc(result)
    if max_val >= threshold:
        h, w = watermark_template.shape[:2]
        return (*max_loc, w, h)
    return None

К обнаруженной зоне применялось две функции - blur + overlay. В результате вотермарка соры менялась на вотермарку моего канала.

Замененная вотермарка
Замененная вотермарка

В процессе тестирования на разных видеороликах выяснилось, что Sora использует 6 разных размеров вотермарки. Для эффективного обнаружения потребовалось добавить в программу все 6 примеров, а также функцию автоматического определения типа вотермарки.

все типы вотермарок
все типы вотермарок
def detect_best_watermark_type(input_path):
    cap = cv2.VideoCapture(input_path)
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    sample_count = min(15, total_frames)  # анализируем первые 15 кадров
    scores = {t: [] for t in W_TYPES}

    for i in range(sample_count):
        ret, frame = cap.read()
        if not ret:
            break
        for t, path in W_TYPES.items():
            tmpl = cv2.imread(path)
            result = cv2.matchTemplate(frame, tmpl, cv2.TM_CCOEFF_NORMED)
            _, max_val, _, _ = cv2.minMaxLoc(result)
            scores[t].append(max_val)

    cap.release()
    avg_scores = {t: np.mean(v) if len(v) > 0 else 0 for t, v in scores.items()}
    best_type = max(avg_scores, key=avg_scores.get)

    if all(val < 0.5 for val in avg_scores.values()):
        best_type = 1

    print(f"[AutoDetect] Selected: {best_type} ({W_TYPES[best_type]}), score={avg_scores[best_type]:.3f}")
    return best_type

Итоговая точность получилась довольно высокая, но для большей эффективности были применены дополнительные оптимизации. В ходе исследования было определено, что позиция вотермарки меняется каждые 67 кадров, поэтому в алгоритм был внедрен автоматический пропуск кадров с простановкой предыдущей зоны, при совпадении зоны на пяти кадрах подрят.

def process_video(input_path, watermark_path, overlay_path, output_path):
    cap = cv2.VideoCapture(input_path)
    video_clip = VideoFileClip(input_path)
    frames = []
    audio = video_clip.audio  # может быть None
    fps = cap.get(cv2.CAP_PROP_FPS)
    watermark_template = cv2.imread(watermark_path)
    overlay_img = cv2.imread(overlay_path, cv2.IMREAD_UNCHANGED)  # RGBA
    frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    positions = {}

    last_pos = (0, 0, 0, 0)
    ident_count = 0
    skip_frames = 0
    for i in range(frame_count):
        ret, frame = cap.read()
        if not ret:
            break
        if skip_frames > 0:
            positions[i + 1] = last_pos
            skip_frames -= 1
            continue
        zone = detect_watermark_zone(frame, watermark_template)
        if not zone:
            positions[i+1] = (0, 0, 0, 0)
            continue
        positions[i+1] = zone
        if last_pos != zone:
            last_pos = zone
            ident_count = 0
        else:
            ident_count += 1
        if ident_count >= 5:
            next_multiple = ((i + 67) // 67) * 67
            skip_frames = min(next_multiple - i, frame_count - i)
            ident_count = 0

В результате, скорость определения зоны вотермарки выросла почти в 10 раз, без снижения точности поиска.

Наложение субтитров поверх видео

Современная молодежь часто плохо фокусирует внимание и постоянно переключается между разными источниками контента. Для удержания внимания было решено внедрить поверх видео субтитры. В качестве основы использовалась библиотека с Github auto-subtitle . Она автоматически генерирует субтитры из голоса с помощью OpenAi Whisper.

Базовые настройки этой библиотеки не удовлетворяли требованиям, которые обычно используются в Tiktok и Shorts, в результате было применено несколько модификаций. Для субтитров был выбран определенный стиль и размер, распространенные у других создателей контента.

        style = (
            "Fontname=Bahnschrift SemiCondensed,"
            'Fontsize=14,'
            "ScaleX=0.8,"
            "Bold=1,"
            "MarginV=100,"
            "MarginL=50,"
            "MarginR=50,"
            "Outline=1.5,"
            'Shadow=0,'
            'BorderStyle=1'
        )

Также был добавлен ограничитель на вывод текста не более 5 слов за раз, чтобы не перегружать видео, и алгоритм раскраски некоторых слов, чтобы субтитры казались более привлекательными.

def color_srt(input_file: str, output_file: str):
    colors = ['lime', 'yellow']

    def colorize_text(text):
        words = text.split()
        n = len(words)
        if n <= 1:
            return text
        elif n == 2:
            words[1] = f'<font color="{random.choice(colors)}">{words[1]}</font>'
        elif n == 3:
            # красим центральное слово
            words[1] = f'<font color="{random.choice(colors)}">{words[1]}</font>'
        else:
            indices = list(range(1, n))
            to_color = random.sample(indices, random.randint(1, 2) if n >= 5 else 1)
            prev_i = None
            prev_color = None
            for i in to_color:
                if prev_i is not None and ((i == prev_i + 1) or (i == prev_i - 1)):
                    color = prev_color
                else:
                    color = random.choice([c for c in colors if c != prev_color]) if prev_color else random.choice(
                        colors)
                words[i] = f'<font color="{color}">{words[i]}</font>'
                prev_i = i
                prev_color = color
        return ' '.join(words)

В результате программа начала стабильно накладывать субтитры поверх каждого видеоролика.

Склейка видео

Алгоритмы youtube shorts и tiktok отдают предпочтения видеороликам продолжительностью 15-30 секунд. Было решено делать видео длительностью 30 секунд на основе двух Sora генераций. Для этого был написан простой модуль на основе Moviepy.

from moviepy import VideoFileClip, concatenate_videoclips


def concat_videos(video_paths, output_path="output.mp4"):
    clips = []
    for path in video_paths:
        clip = VideoFileClip(path)
        if clip.duration > 1:
            clip = clip.subclipped(0, clip.duration)
        clips.append(clip)

    final_clip = concatenate_videoclips(clips)
    final_clip.write_videofile(output_path, codec="libx264", audio_codec="aac")
    for c in clips:
        c.close()
    final_clip.close()

После этого была написана функция, объединяющая эти три операции в одну.

def mounting_video(video_name: str, subt_off=False):
    video_count = VIDEO_COUNT
    w_types = [0, 0, 0, 0]
    fix_only = (False, 3)
    for i in range(1, video_count+1):
        if fix_only[0]:
            i = fix_only[1]
        print(f'{i} видео начало обработку')
        if w_types[i-1] != -1:
            hide_watermark(PATHS.input + f'{i}.mp4', w_types[i-1])
        else:
            shutil.copy(PATHS.input + f'{i}.mp4', f'./input/m_{i}.mp4')
        if not subt_off:
            sys.argv = ['mounting_video.py', PATHS.tmp + f'm_{i}.mp4', '-o', PATHS.tmp]
            main()
        else:
            shutil.copy(PATHS.tmp + f'm_{i}.mp4', PATHS.tmp + f'sm_{i}.mp4')
        if fix_only[0]:
            break

    videos = [PATHS.tmp + f'sm_{i}.mp4' for i in range(1, video_count+1)]
    concat_videos(videos, PATHS.output + video_name)


if __name__ == '__main__':
    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M")
    filename = f"file_{timestamp}.mp4"
    mounting_video(filename)

Изначально эта функция была рассчитана на склейку четырех видеороликов по 10 секунд, в процессе оптимизаций было решено заменить их количество на 2 по 15 секунд.

Автоматическая генерация сюжетов

Сначала я вручную писал промпты в чат гпт и вставлял ответы в сору. Через несколько дней мне это надоело, и я начал автоматизировать этот процесс с помощью библиотеки g4f.

def get_sora_prompts(pr_text):
    # new_text = prompt_modifications(pr_text)
    new_text = pr_text

    good_providers = [
        OIVSCodeSer0501
    ]
    print('Generate new prompt')
    response = client.chat.completions.create(
        model=g4f.models.default,
        provider=random.choice(good_providers),
        messages=[{"role": "user", "content": new_text}],
        web_search=False
    )
    print(f'Selected provider: {response.provider}')
    answer = response.choices[0].message.content
    start = answer.find('{')
    end = answer.rfind('}') + 1
    if start == -1 or end <= 0:
        return False
    js_text = answer[start:end]
    try:
        data = json.loads(js_text)
    except json.JSONDecodeError:
        return False
    required_keys = {"prompts", "title", "description", "tags"}
    if not required_keys.issubset(data.keys()):
        return False
    if not isinstance(data["prompts"], list) or len(data["prompts"]) != 2:
        return False
    for item in data["prompts"]:
        if not isinstance(item, str):
            return False
    print('Prompt successfully validated')
    save_correct_prompt(data)
    return True

Промпт составлен так, чтобы нейросеть возвращала ответ в следующем формате:

{
  "prompts": [
    "текст первого промпта",
    "текст второго промпта"
  ],
  "title": "кликбейтное название для видео + 3 тега через #",
  "description": "описание видео",
  "tags": "теги через запятую без #"
}

Это позволяет легко распарсить полученный json и провести несколько тестов на валидацию, что нейросеть не прислала в ответ отсебятину. Результат сохраняется в файл для дальнейшего использования.

Автоматическая генерация Sora

После автоматизации промптов напрашивалось автоматическое управление сайтом Sora, чтобы не тратить время на ручной ввод и загрузку результатов. Автоматизация проводилась средствами Python Playwright с аддоном stelth для обхода защиты cloudflare. Код получился довольно громоздким, посмотреть его вы можете на моем github. В программу заложена отказоустойчивость на случай перегрузки сервера или блокировки генерации за нарушение правил.

Написанный модуль использует два аккаунта параллельно через куки файлы, вводит в них промпты и качает полученные видео. Таким образом он делает 4 видеоролика по 15 секунд каждые 5 минут. Возможно дальнейшее масштабирование через увеличение количества аккаунтов.

Объединение модулей

После написания всех модулей я объединил их одной общей функцией, которая генерирует 10 видеороликов на одну кнопку.

import asyncio
import os
import time
from datetime import datetime
import shutil
from constants import PATHS, VIDEO_COUNT, FOLDER_VID_COUNT
from gpt_module import create_new_prompts
from sora_module import create_sora_videos
from mounting_video import mounting_video


def generate_video():
    subt_off = False

    create_new_prompts()
    is_all_videos = False
    while not is_all_videos:
        is_all_videos = asyncio.run(create_sora_videos())
        time.sleep(5)
    cur_date = datetime.now().strftime("%d%m")
    folder_name = cur_date
    folder_path = os.path.join(PATHS.sora_videos, folder_name)
    for i in range(1, FOLDER_VID_COUNT+1):
        result_name = f'{i}_{cur_date}.mp4'
        result_file = os.path.join(PATHS.output, result_name)
        if os.path.exists(result_file):
            print(f'Видео {i} уже создано')
            continue
        videos_path = os.path.join(folder_path, str(i))
        if len(os.listdir(videos_path)) < VIDEO_COUNT:
            print(f'Пропуск видео {i}, нехватает кусков')
            continue
        for filename in os.listdir(videos_path):
            source_file = os.path.join(videos_path, filename)
            destination_file = os.path.join(PATHS.input, filename)
            shutil.copy(source_file, destination_file)
        mounting_video(result_name, subt_off)


if __name__ == '__main__':
    generate_video()

В результате всего одно нажатие приводит к созданию 10 видеороликов длительность по 30 секунд каждый, с неповторяющимися уникальными сюжетами.

Создание всех видеороликов занимает примерно 30 минут. Основное время уходит на ожидание генераций от Sora2. При желании это можно масштабировать и параллелить, используя больше аккаунтов и проксей.

Автоматическая загрузка на YouTube

Последним шагом для полной автоматизации напрашивается загрузка видеороликов на Youtube и Tiktok. Это позволит запустить систему на удаленном сервере и забыть о ней на пол года, пока аккаунты набирают аудиторию без участия человека.

В автоматизации Youtube есть две методики. Первая это использование официального Api. Есть официальный репозиторий с подобным кодом. У этого метода есть недостаток - все видео попадают в черновик, а попытка публикации через Api приводит к автоматической блокировке видео. Это можно обойти договорившись с техподдержкой, но делать это я не пробовал.

Второй метод это автоматизация средствами Selenium или Playwright, в настоящее время я ещё не написал этот модуль, но он планируется к внедрению в ближайшие недели.

Итоги

В данный момент я веду каналы на Youtube и Tiktok, загружая в них по 10 видеороликов в день. Проект существует с 5 октября, поэтому качественной статистики пока нет. Алгоритм ютуб в среднем рекомендует 3 видео из 10, обеспечивая им примерно тысячу просмотров. Некоторые видео начинают рекомендоваться после задержки в несколько недель, повышая общие просмотры. В тикток рекомендуется почти каждое видео, но средние просмотры держатся в районе 300.

Проект пока недоступен на github, каждый день в него вносятся сильные корректировки. Также меняется системный промпт для тематики видеороликов, в поисках более качественных тем для повышения просмотров.

Комментарии (5)


  1. blacksan
    20.10.2025 08:44

    По просмотрам чет кажется не все хорошо, но в целом интересно.


  1. SvetlanaDen
    20.10.2025 08:44

    Интересно, но разве в пользовательском соглашении Sora не стоит запрет на удаление ватермарки?


  1. Ryav
    20.10.2025 08:44

    У хабровчан появилась уникальная возможность высказать всё, что они думают, тому, кто «загрязняет» интернет.


  1. Rubilnik
    20.10.2025 08:44

    Эх, как говорится "Ваш энтузиазм, да в правильное русло..."


  1. savostin
    20.10.2025 08:44

    Так вот откуда этот шлак берется в моей ленте.