Сборка вставки не начинается заново от каждой правки

Затухание в семьдесят секунд не собиралось никогда: сборка сведённого
куска шла полторы минуты, и любая правка в это время сбрасывала её на
ноль. В журнале это видно как «Расстановка изменилась» и тот же самый
запуск ffmpeg по кругу.

Причина — в вопросе, который задавался. «Нужна ли ещё эта работа»
проверялось по списку недостающего, а недостающее у нас — это то, чего
нет на диске. У куска, который делается прямо сейчас, файл уже есть:
ffmpeg создаёт его в первую же секунду. Кусок в список не попадал, ответ
был «не нужна», работа отменялась.

Теперь уплощение отдельно отдаёт список нужного — независимо от того,
лежит файл на диске или ещё пишется. Заодно закрыта та же дыра со
вставками тишины: проверка их вообще не видела, потому что звуковые
дорожки считаются отдельным проходом.

Проверено обеими сторонами (tests/genrestart.sh, на настоящем материале,
затухание 70 с): пять резов в стороне от затухания — один запуск и ни
одной отмены, было шесть запусков и десять отмен. Удалили всё — работа
прекращается за две секунды, как и прежде.
This commit is contained in:
4aiman 2026-10-01 00:47:28 +03:00
commit d2a49343c3
4 changed files with 229 additions and 7 deletions

View file

@ -840,6 +840,16 @@ Wine с новой библиотекой не грузилось ничего.
привычный RGB. Записанный по привычке код даёт цвет с переставленными
красным и синим.
**Список недостающего — не список нужного.** Недостающее у нас — это то,
чего нет на диске. Но у куска, который делается прямо сейчас, файл уже
есть: ffmpeg создаёт его в первую же секунду и пишет до конца. Проверка
«нужна ли ещё эта работа» спрашивала по списку недостающего — и отвечала
«не нужна» про то, что делается. Работа отменялась в разгаре и начиналась
заново. На коротком затухании это проходило незаметно, на затухании в
семьдесят секунд сборка не заканчивалась никогда: любая правка сбрасывала
её. Теперь уплощение отдельно отдаёт список нужного, независимо от того,
лежит файл на диске или ещё пишется (`tests/genrestart.sh`).
**MousePos у колеса мыши — не экранный.** По описанию `DoMouseWheel`
получает точку в координатах экрана, но под gtk2 в ней уже клиентские:
`ScreenToClient` переводит их второй раз, и точка уезжает влево ровно на
@ -935,6 +945,21 @@ Wine с новой библиотекой не грузилось ничего.
уход выглядит как накапливающийся рассинхрон, которого нет. На это уже
попались один раз.
Ещё три проверки гоняют саму программу на невидимом экране (`Xvfb :99`).
Рабочий экран они не занимают:
```
./tests/buttonclicks.sh <окно> <x> <y> каждый щелчок по кнопке считается
./tests/zoomanchor.py <окно> масштаб берётся от точки под мышкой
./tests/genrestart.sh [<путь к nlve>] сборка вставки не сбрасывается зря
```
Последней нужен проект с одним клипом и долгим — секунд на семьдесят —
затуханием: такой кусок сводится около полутора минут, и всё это время
человек продолжает работать. Путь к проекту задаётся переменной
`NLVE_FADE_PROJECT`; в репозиторий он не входит, потому что ссылается на
съёмку, которой здесь нет.
## Структура
| Файл | Что это |

View file

@ -119,7 +119,8 @@ function FlattenStream(AProject: TProject; AKind: TTrackKind;
списком кусков, а дальше они складываются его же средствами. }
function FlattenAudioTrack(AProject: TProject; ATrack: TTrack;
const ATotal: Double;
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests): Boolean;
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests;
AWanted: TStrings = nil): Boolean;
{ Длина слышимого материала одного вида дорожек. }
function AudibleEnd(AProject: TProject; AKind: TTrackKind): Double;
@ -196,9 +197,14 @@ function GeneratedPath(const ACacheDir: string; const AReq: TGenRequest): string
Для сборки ролика это не применяется: там наложение считается заново с
параметрами вывода, и промежуточный файл был бы лишней потерей
качества. }
качества.
AWanted — куда сложить пути всех сведённых кусков, которые этой
расстановке нужны, независимо от того, лежат они уже на диске или ещё
делаются. По списку недостающего этого не узнать: файл появляется в
первую же секунду работы ffmpeg и пишется до конца. }
procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray;
var AMissing: TGenRequests);
var AMissing: TGenRequests; AWanted: TStrings = nil);
{ Заготовка чёрной вставки самой ходовой длины.
@ -416,7 +422,7 @@ begin
end;
procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray;
var AMissing: TGenRequests);
var AMissing: TGenRequests; AWanted: TStrings);
var
I: Integer;
Req: TGenRequest;
@ -443,6 +449,16 @@ begin
Req.TlEnd := At + ASegments[I].Duration;
At := Req.TlEnd;
{ Список нужного — это не то же самое, что список недостающего.
Недостающее — чего нет на диске. Но пока кусок делается, файл уже
есть: ffmpeg создаёт его сразу и пишет в него до самого конца.
Спрашивать «нужен ли ещё тот кусок, что сейчас делается» по списку
недостающего бессмысленно — его там не будет, и работа отменится в
разгаре. }
if AWanted <> nil then
AWanted.Add(Req.TargetPath);
if not FileExists(Req.TargetPath) then
begin
SetLength(AMissing, Length(AMissing) + 1);
@ -1115,7 +1131,8 @@ end;
function FlattenAudioTrack(AProject: TProject; ATrack: TTrack;
const ATotal: Double;
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests): Boolean;
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests;
AWanted: TStrings): Boolean;
var
C, N: Integer;
At: Double;
@ -1160,6 +1177,11 @@ var
Req.Duration := BucketFor(Take);
Req.TargetPath := GeneratedPath(AProject.CacheDir, Req);
Seg.FilePath := Req.TargetPath;
{ Нужное и недостающее — разные списки: см. пояснение у
ResolveLayers. Кусок, который делается прямо сейчас, на диске уже
есть, и в недостающее он не попадёт. }
if AWanted <> nil then
AWanted.Add(Req.TargetPath);
{ Один и тот же файл не заказываем дважды: длинная тишина набирается
повторами, и без этой проверки минутный кусок делался бы столько
раз, сколько его повторов. }

View file

@ -2327,11 +2327,29 @@ end;
одна из готовящихся вставок в новом списке не нужна, работу можно
прекращать. Ошибиться тут не страшно: отменённое, если оно всё же
понадобится, закажут заново на следующей пересборке. }
{ Нужна ли ещё та работа, что идёт сейчас.
Спрашивать по одному только списку недостающего нельзя, и это стоило
повисшей сборки. Недостающее — то, чего нет на диске; а у куска,
который делается прямо сейчас, файл уже есть: ffmpeg создаёт его в
первую же секунду и пишет до конца. Поэтому сведённый кусок в списке
недостающего не появлялся, проверка отвечала «не нужен», работа
отменялась в разгаре — и начиналась заново с нуля.
На коротком затухании это проходило незаметно: кусок успевал
посчитаться между правками. На затухании в семьдесят секунд, которое
считается полторы минуты, любая следующая правка сбрасывала работу, и
сборка не заканчивалась никогда.
Поэтому спрашиваем иначе: нужен ли расстановке сам этот путь — неважно,
лежит он уже на диске или ещё пишется. }
function TMainForm.GenerationStillNeeded: Boolean;
var
Segs, Auds: TFlatSegmentArray;
Segs, Auds, TrackSegs: TFlatSegmentArray;
Ovl: TPreviewOverlays;
Missing: TGenRequests;
Missing, TrackMissing: TGenRequests;
Wanted: TStringList;
TotalLen: Double;
I, J: Integer;
begin
Result := False;
@ -2341,6 +2359,33 @@ begin
FSettings.PreviewKey) then
Exit;
Wanted := TStringList.Create;
try
ResolveLayers(FProject, Segs, Missing, Wanted);
{ Звуковые дорожки считаются тем же путём, что и при пересборке:
вставки тишины под дыры у каждой дорожки свои, и они тоже могут
делаться прямо сейчас. }
TotalLen := Max(AudibleEnd(FProject, tkVideo), AudibleEnd(FProject, tkAudio));
for I := 0 to FProject.TrackCount - 1 do
begin
if FProject.Tracks[I].Kind <> tkAudio then
Continue;
if not FProject.TrackAudible(FProject.Tracks[I]) then
Continue;
if FProject.Tracks[I].Count = 0 then
Continue;
FlattenAudioTrack(FProject, FProject.Tracks[I], TotalLen,
TrackSegs, TrackMissing, Wanted);
end;
for J := 0 to FGenerate.TargetCount - 1 do
if Wanted.IndexOf(FGenerate.TargetPath(J)) >= 0 then
Exit(True);
finally
Wanted.Free;
end;
for I := 0 to High(Missing) do
for J := 0 to FGenerate.TargetCount - 1 do
if Missing[I].TargetPath = FGenerate.TargetPath(J) then

130
tests/genrestart.sh Executable file
View file

@ -0,0 +1,130 @@
#!/bin/sh
# Сборка сведённого куска не должна начинаться заново от каждой правки.
#
# Затухание, наплыв, прозрачность — всё это предпросмотр не умеет играть
# как есть: такой кусок заранее сводится в отдельный файл. На затухании в
# семьдесят секунд файл считается около полутора минут, и всё это время
# человек продолжает работать.
#
# Проверяется два обратных случая:
# 1. Режем монтаж в стороне от затухания. Сам кусок от этого не
# меняется — значит сборка обязана продолжаться, а не начинаться
# заново.
# 2. Удаляем всё. Кусок стал не нужен — сборка обязана прекратиться.
#
# Раньше первый случай отменял работу: «нужен ли ещё этот кусок»
# спрашивалось по списку недостающего, а у куска, который делается прямо
# сейчас, файл уже есть — ffmpeg создаёт его сразу. Кусок в список не
# попадал, работа отменялась в разгаре и начиналась с нуля.
#
# Запуск: tests/genrestart.sh [<путь к nlve>]
# Проект берётся из test/поток/длинный-фейд.nlve (см. README, раздел
# проверок): один клип с затуханием в семьдесят секунд.
set -e
cd "$(dirname "$0")/.."
BIN=${1:-$PWD/nlve}
DISP=${NLVE_DISPLAY:-:99}
# Проект для проверки в репозиторий не входит: он ссылается на съёмку,
# которой в нём нет. Годится любой с одним клипом и долгим затуханием —
# путь можно задать переменной.
PROJ=${NLVE_FADE_PROJECT:-$PWD/test/поток/длинный-фейд.nlve}
LOG=$(dirname "$BIN")/nlve.log
[ -f "$PROJ" ] || { echo "нет проекта для проверки: $PROJ"; exit 2; }
start() {
pkill -x nlve 2>/dev/null || true
# Закрытая программа не уносит с собой свой ffmpeg: дочерний процесс
# остаётся и продолжает писать в кэш. Убираем только его — по пути
# этого самого проекта, чтобы не задеть чужую работу.
pkill -f "длинный-фейд.nlve-cache" 2>/dev/null || true
sleep 1
rm -rf "$PWD/test/поток/длинный-фейд.nlve-cache" "$PWD/test/поток/профиль-$1"
rm -f "$PROJ.autosave"
SP=$PWD/test/поток/профиль-$1
mkdir -p "$SP"
: > "$LOG"
XDG_CONFIG_HOME=$SP XDG_CACHE_HOME=$SP XDG_DATA_HOME=$SP DISPLAY=$DISP \
"$BIN" "$PROJ" >/dev/null 2>&1 &
sleep 16
WIN=$(DISPLAY=$DISP xdotool search --name "длинный-фейд" | tail -1)
DISPLAY=$DISP xdotool windowmove "$WIN" 0 0 windowsize "$WIN" 1500 860
sleep 2
}
runs() { grep -c filter_complex "$LOG" || true; }
stops() { grep -c 'Расстановка изменилась' "$LOG" || true; }
BAD=0
echo "1. Правки в стороне от затухания"
start правки
echo " сборка началась: запусков $(runs)"
X=650
I=0
while [ $I -lt 5 ]; do
DISPLAY=$DISP xdotool mousemove $X 640 click 1
sleep 0.6
DISPLAY=$DISP xdotool key s
sleep 3
X=$((X + 40))
I=$((I + 1))
done
sleep 5
N=$(runs)
S=$(stops)
echo " после пяти резов: запусков $N, отмен $S"
case "$(DISPLAY=$DISP xdotool getwindowname "$WIN")" in
*\*) ;;
*) echo " ПЛОХО: монтаж не изменился, правки не дошли — проверка ничего не значит"
BAD=$((BAD + 1)) ;;
esac
if [ "$N" = "1" ] && [ "$S" = "0" ]; then
echo " ok: работа не сбрасывалась"
else
echo " ПЛОХО: сборка начиналась заново"
BAD=$((BAD + 1))
fi
echo
echo "2. Кусок стал не нужен"
start отмена
echo " сборка началась: запусков $(runs)"
DISPLAY=$DISP xdotool mousemove 700 600 click 1
sleep 0.5
DISPLAY=$DISP xdotool key ctrl+a
sleep 0.8
DISPLAY=$DISP xdotool key Delete
# Ждём, пока работа прекратится, а не спим наугад: ffmpeg получает сигнал
# и ещё дописывает то, что у него в руках. Именно -x у pgrep: иначе под
# раздачу попадёт и сама эта оболочка, у которой слово ffmpeg есть в
# командной строке.
ALIVE=1
WAITED=0
while [ "$WAITED" -lt 30 ]; do
sleep 1
WAITED=$((WAITED + 1))
ALIVE=$(pgrep -a -x ffmpeg | grep -c "длинный-фейд.nlve-cache" || true)
[ "$ALIVE" = "0" ] && break
done
S=$(stops)
echo " отмен $S, живых ffmpeg $ALIVE, ждали $WAITED с"
if [ "$S" -ge 1 ] && [ "$ALIVE" = "0" ]; then
echo " ok: ненужная работа прекращена"
else
echo " ПЛОХО: работа продолжается впустую"
BAD=$((BAD + 1))
fi
pkill -x nlve 2>/dev/null || true
pkill -f "длинный-фейд.nlve-cache" 2>/dev/null || true
rm -f "$PROJ.autosave"
echo
if [ "$BAD" = "0" ]; then
echo "ВЕРДИКТ: сборка вставок не сбрасывается зря и не идёт впустую"
else
echo "ВЕРДИКТ: не пройдено случаев: $BAD"
exit 1
fi