Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d2afe53
Record other programs' GPU share for each sampling step in the diagno…
KBRASK Oct 16, 2026
28b08b9
Export dialog sizes use the same even-size rounding as the saved work
KBRASK Oct 16, 2026
1d969a6
Editor text: clicking the side Content box right after T keeps the ne…
KBRASK Oct 16, 2026
4c329a8
Editor text: dropping onto a locked text track says the track is locked
KBRASK Oct 16, 2026
d20c1ba
Long videos on Mac: the first part no longer asks for a hidden tail
KBRASK Oct 16, 2026
564ceb8
Editor: dragging a range's left edge to the timeline start no longer …
KBRASK Oct 16, 2026
d12a698
Editor: splitting a sped-up or reversed part keeps each fade on its o…
KBRASK Oct 16, 2026
6049b7e
Localize browser opening failure guidance
KBRASK Oct 16, 2026
2b698e6
Keep canvas margin labels readable and aligned
KBRASK Oct 16, 2026
a6f8165
Nodes: Chinese labels for width, height, duration and seed
KBRASK Oct 16, 2026
8f3af7e
Switch every entry to the new quality levels
KBRASK Oct 16, 2026
eb50c37
Decoder read-ahead on Windows follows physical RAM and starts earlier…
KBRASK Oct 16, 2026
d7a1ee7
Name the launch visit script in the page check
KBRASK Oct 16, 2026
42510d2
Enable expanded editing for long-video part descriptions
KBRASK Oct 16, 2026
502a580
Offer a new chat when a conversation is full
KBRASK Oct 16, 2026
5c60a86
Keep editor selection updates cheap by replacing two :has() rules wit…
KBRASK Oct 16, 2026
596364d
Progress estimate: weigh reference rows by canvas size
KBRASK Oct 16, 2026
8d76851
Treat localized Windows commit exhaustion as a host memory failure
KBRASK Oct 16, 2026
3def11d
Release 0.4.9
KBRASK Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions freevideo_engine/adaptive.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def cuda_runtime_error(error):
return False


def commit_exhausted(error):
from .diagnostic_resources import commit_exhausted as found
return found(error)


def classify_failure(error, metrics=None):
"""Only positively identified resource failures are retriable.

Expand Down Expand Up @@ -63,7 +68,7 @@ def classify_failure(error, metrics=None):
kind, outcome, retry = 'timeout', 'cancelled', False
elif details.get('kind') == 'gpu_oom' or 'cuda out of memory' in text or 'cuda error: out of memory' in text:
kind, outcome, retry = 'gpu_oom', 'resource_failure', True
elif isinstance(error, MemoryError) or details.get('kind') == 'ram_pressure':
elif isinstance(error, MemoryError) or details.get('kind') == 'ram_pressure' or commit_exhausted(error):
kind, outcome, retry = 'ram_pressure', 'resource_failure', True
elif isinstance(error, WorkerExit) and not (metrics or {}).get('error'):
kind, outcome, retry = 'unknown_worker_exit', 'discarded', False
Expand Down Expand Up @@ -434,7 +439,12 @@ def change(section, name, new):
host_pins = failure['kind'] == 'gpu_oom' and nonlocal_exhausted(failure)
compute = None
if failure['kind'] == 'gpu_oom':
if host_pins and phase in ('load', 'sample') and (engine.get('pin_host_gb', 0.) > 0 or engine.get('pin_host_weights')):
if phase == 'sample' and engine.get('step_cache') == 'fbcache' and not host_pins:
# Its two packed residuals are the newest GPU allocation in the
# pass; sample every step in full before moving any weights.
changes['engine.step_cache'] = {'from': 'fbcache', 'to': None}
del engine['step_cache']
elif host_pins and phase in ('load', 'sample') and (engine.get('pin_host_gb', 0.) > 0 or engine.get('pin_host_weights')):
# Page-locked memory ran out, not VRAM: return pinned weights and
# keep every GPU setting. Halve once, then pin nothing.
change('engine', 'pin_host_gb', 0. if previous_gpu_retries else round(engine.get('pin_host_gb', 0.) / 2, 3))
Expand Down
4 changes: 2 additions & 2 deletions freevideo_engine/agent/editor_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
# import does. editor_model and editor_text_model import each other: the
# module still loading is reached only through a namespace import.
MODULES = ('editor_picture_fields', 'editor_transport_model', 'editor_audio_model', 'editor_speed_model',
'editor_text_model', 'editor_model', 'editor_room', 'editor_actions', 'editor_generation', 'chain_support',
'editor_store')
'editor_text_model', 'editor_model', 'editor_room', 'editor_actions', 'editor_generation', 'sampling_effort',
'chain_support', 'editor_store')
TIME_LIMIT = 5.0
MEMORY_LIMIT = 256 * 2**20
STACK_LIMIT = 4 * 2**20
Expand Down
6 changes: 4 additions & 2 deletions freevideo_engine/agent/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@
'the draft and looks plainer than a direct generation, so keep the direct one when the user prefers its look.',
],
'quality': [
'Steps per quality: "light" = 8 first-pass steps + 3 refine steps, two-pass (default); "medium" = 12 steps, '
'"high" = 16 steps, "max" = 20 steps, each in one pass.',
'Steps per quality: "light" = 8 first-pass steps + 3 refine steps, two-pass (default); "medium" = 8 steps, '
'"high" = 20 steps with a step cache, "max" = 20 steps with a step cache that skips less work, each in one pass. '
'"max" runs plain 20 steps when the GPU has no room for its cache. On a Mac, "high" is 16 steps and '
'"max" 20 steps, without a cache.',
'Two-pass generation runs only with 8 first-pass steps. Any other base_steps (for example 20 with '
'two_pass=true) runs in one pass of that many steps; the job notice says so. A preview is always two-pass '
'with 8 steps.',
Expand Down
11 changes: 8 additions & 3 deletions freevideo_engine/agent/length.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ def single_frame_limit(spec, *, port=None, table=None):
tiers = table.get('tiers', {})
tier = tiers.get(spec['quality'])
if tier is None:
tier = next((row for row in tiers.values()
if row.get('steps') == spec['base_steps'] and row.get('two_pass') == spec['two_pass']
and (not spec['two_pass'] or spec['refine_steps'] == 3)), {})
# The same plan with the same step cache first. A cache keeps the length a
# card fits (the planner drops a first-block cache that does not fit),
# so the same plan with another cache is the next best row.
same = [row for row in tiers.values()
if row.get('steps') == spec['base_steps'] and row.get('two_pass') == spec['two_pass']
and (not spec['two_pass'] or spec['refine_steps'] == 3)]
tier = next((row for row in same if row.get('step_cache') == spec.get('step_cache')),
same[0] if same else {})
index = next((i for i, row in enumerate(table.get('sizes', []))
if (row['width'], row['height']) == (spec['width'], spec['height'])), None)
if index is None:
Expand Down
9 changes: 6 additions & 3 deletions freevideo_engine/agent/productions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,9 +1012,12 @@ def _too_long(self, item, resolved):
tiers = table.get('tiers', {})
tier = tiers.get(normalized['quality'])
if tier is None:
tier = next((row for row in tiers.values()
if row.get('steps') == normalized['base_steps'] and row.get('two_pass') == normalized['two_pass']
and (not normalized['two_pass'] or normalized['refine_steps'] == 3)), None)
# Same plan and step cache first, then the same plan (see length.single_frame_limit).
same = [row for row in tiers.values()
if row.get('steps') == normalized['base_steps'] and row.get('two_pass') == normalized['two_pass']
and (not normalized['two_pass'] or normalized['refine_steps'] == 3)]
tier = next((row for row in same if row.get('step_cache') == normalized.get('step_cache')),
same[0] if same else None)
maxima = tier.get('max_frames') if tier else None
if index is None or not isinstance(maxima, list) or index >= len(maxima):
return False
Expand Down
4 changes: 3 additions & 1 deletion freevideo_engine/agent/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from . import assets
from . import private_file
from . import spec as specs
from .. import tiers as quality_tiers
from . import schema as contract, errors, access
from .jobs import ACTIVE, DONE, JobError, Jobs, chain_error, library_view

Expand Down Expand Up @@ -213,7 +214,8 @@ def capabilities(port, jobs):
seconds_tested=[4, 15],
fps=24, base_steps=[1, 32], refine_steps=[1, 31], references=32, loras=32, lora_strength=[-4, 4],
upload_bytes=2 * 1024 ** 3, vlm_images=24, takes=8, batch_jobs=64),
quality={name: dict(two_pass=plan[0], base_steps=plan[1], refine_steps=plan[2])
quality={name: dict(two_pass=plan[0], base_steps=plan[1], refine_steps=plan[2],
step_cache=quality_tiers.step_cache(name, quality_tiers.on_mac()))
for name, plan in specs.QUALITY.items()},
aspects=sorted(specs.ASPECTS), native_megapixels=.98,
tasks=GUIDE['tasks'], media_extensions={k: sorted(v) for k, v in EXTENSIONS.items()},
Expand Down
7 changes: 5 additions & 2 deletions freevideo_engine/agent/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
import re

from . import API_VERSION
from .. import tiers

S = 'https://json-schema.org/draft/2020-12/schema'
PREFIX = '/freevideo/agent/v1'
QUALITY = {'light': (True, 8, 3), 'medium': (False, 12, 3), 'high': (False, 16, 3), 'max': (False, 20, 3)}
# (two_pass, base_steps, refine_steps) of each level on this platform; the step
# cache a level adds is tiers.step_cache(name, mac) (none on Mac).
QUALITY = {name: tiers.plan(name, mac=tiers.on_mac()) for name in tiers.TIERS}
ASPECTS = {'16:9': (16, 9), '9:16': (9, 16), '1:1': (1, 1), '4:3': (4, 3), '3:4': (3, 4),
'21:9': (21, 9), '2:3': (2, 3), '3:2': (3, 2)}
IMAGE_ASPECTS = {'1:1': (1024, 1024), '16:9': (1344, 768), '9:16': (768, 1344),
Expand Down Expand Up @@ -191,7 +194,7 @@ def generate(*, local=False):
{'required': ['sampling', 'quality'], 'properties': {
'sampling': {'required': ['two_pass'], 'not': {'required': ['base_steps']},
'properties': {'two_pass': {'const': True}}},
'quality': {'enum': ['medium', 'high', 'max']}}}]}}}],
'quality': {'enum': [name for name, plan in QUALITY.items() if plan[1] != 8]}}}]}}}],
description='x-job-kind selects normalization and checks derived geometry and sampling step comparisons; these calculations are not JSON Schema constraints.',
**{'x-job-kind': 'generate'})

Expand Down
11 changes: 9 additions & 2 deletions freevideo_engine/agent/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ def canvas_size(value):


def sampling(value):
from .. import tiers
mac = tiers.on_mac()
quality = value.get('quality', contract.generate()['properties']['quality']['default'])
two, base, refine = QUALITY[quality]
custom = value.get('sampling', {})
Expand All @@ -136,10 +138,13 @@ def sampling(value):
# Only the 8-step first pass goes two-pass; any other count is one pass, said once in the answer.
note = one_pass_note(base)
two = two and note is None
# A level's step cache stays only with that level's own plan; custom sampling runs every step.
cache = tiers.step_cache(quality, mac) if QUALITY[quality] == (two, base, refine) else None
if custom:
quality = next((k for k, v in QUALITY.items() if v == (two, base, refine)), 'custom')
quality = next((k for k, v in QUALITY.items() if v == (two, base, refine)
and tiers.step_cache(k, mac) == cache), 'custom')
return dict(quality=quality, two_pass=two, base_steps=base, refine_steps=refine,
**({'sampling_note': note} if note else {}))
**({'step_cache': cache} if cache else {}), **({'sampling_note': note} if note else {}))


def generation_media(value):
Expand Down Expand Up @@ -276,6 +281,8 @@ def comfy_prompt(spec, node, *, prompt, assets, refine_from=''):
if spec.get('first_frame_reference'):
# Every part of the video carries it; the part that continues nothing ignores it.
inputs['first_frame_reference'] = True
if spec.get('step_cache'):
inputs['step_cache'] = spec['step_cache']
graph = {node: {'class_type': 'FreeVideoGenerate', 'inputs': inputs,
'_meta': {'title': 'FreeVideo · ' + (spec.get('label') or 'Agent job')}}}
if assets:
Expand Down
25 changes: 17 additions & 8 deletions freevideo_engine/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
from .policy import BLOCK_BYTES, ResourceBudgetError, choose
from .stage_estimate import STAGES, _params, gpu_key, predict_prior
from .two_pass import first_pass_policy, plan as sampling_plan
from . import tiers as quality_tiers

# Shared by all queries, including the default summary; parity with the editor
# is checked in test_capabilities (JS uses positive Math.round, not bankers' rounding).
SIZE_PRESETS = (.5, .75, .98, 1., 1.5, 2.)
TIERS = {'light': (8, True), 'medium': (12, False),
'high': (16, False), 'max': (20, False)}
TIERS = {name: (tier['steps'], tier['two_pass']) for name, tier in quality_tiers.TIERS.items()}
PLAN_NAMES = {'-1': 'refused', '0': 'normal', '1': 'slower_streaming',
'2': 'admitted_tight_memory'}
# WholeVideoProgress.for_request starts an ordinary new-prompt request with
Expand Down Expand Up @@ -242,7 +242,12 @@ def _request(width, height, frames, steps, two_pass, task='t2va', refine=3,


def _admit(machine, width, height, frames, steps, two_pass, task='t2va', refine=3,
references=None, reference_rows=None):
references=None, reference_rows=None, step_cache=None):
"""Admission of one request; `step_cache` asks the planner for a first-pass cache.

The request carries the cache the planner keeps (never one on Mac), so its
estimate and history follow what the engine will actually run.
"""
canvas, sampling, request = _request(width, height, frames, steps, two_pass, task, refine,
references, reference_rows)
rows = dict(video=canvas.get('reference_video_tokens', 0), audio=canvas.get('reference_audio_tokens', 0))
Expand All @@ -269,7 +274,9 @@ def _admit(machine, width, height, frames, steps, two_pass, task='t2va', refine=
machine['ram'], machine['ram_available'], system=machine['system'])
options = dict(precision=machine['precision'], attention='sage2/sage2', available_backends={'sage2'})
choose(hardware, canvas=canvas, stage='encoding', **options)
final = choose(hardware, canvas=canvas, **options)
final = choose(hardware, canvas=dict(canvas, step_cache=step_cache) if step_cache else canvas, **options)
if final.engine.get('step_cache'):
request['step_cache'] = final.engine['step_cache']
first = first_pass_policy(final.legacy_profile(), canvas, sampling)
engines = [first['profile']['engine'] if first else final.engine, final.engine]
request.update(pass1_resident_blocks=engines[0]['resident_blocks'],
Expand Down Expand Up @@ -327,7 +334,7 @@ def ratios(rows):
request, _, _ = _admit(machine, shape['width'], shape['height'], shape['frames'],
row['base_steps'], row['two_pass'], row['task'],
row['refine_steps'] if row['two_pass'] else 3,
reference_rows=refs)
reference_rows=refs, step_cache=row.get('step_cache'))
if request is None:
continue
request['lora_count'] = int(bool(row.get('adapters')))
Expand Down Expand Up @@ -355,6 +362,7 @@ def _whole_records(records, request, adapters):
and r.get('flow', 'full') == 'full' and r.get('task') == request['task']
and r.get('adapters', False) == adapters and r.get('two_pass') == request['two_pass']
and r.get('base_steps') == request['base_steps'] and r.get('refine_steps') == request['refine_steps']
and r.get('step_cache') == request.get('step_cache')
and ((request['task'] == 't2va' and r.get('reference_rows') is None)
or (valid_reference_rows(r.get('reference_rows')) and r['reference_rows'] == rows))]

Expand Down Expand Up @@ -407,11 +415,12 @@ def estimate(request):
return local.get('seconds', prior['total']['seconds'] * ratio)

tiers = {}
for name, (steps, two_pass) in TIERS.items():
for name, tier in quality_tiers.table(machine['mac']).items():
steps, two_pass, step_cache = tier['steps'], tier['two_pass'], tier['step_cache']
times, codes, maxima, frame_maxima, limits = [], [], [], [], []
for size_row in sizes:
admitted = {f: _admit(machine, size_row['width'], size_row['height'], f, steps, two_pass, task,
references=references)
references=references, step_cache=step_cache)
for f in range(39, lengths[-1]['frames'] + 1, 17)}
estimates = {}
for f, (request, _, _) in admitted.items():
Expand All @@ -437,7 +446,7 @@ def estimate(request):
row_codes.append(code)
times.append(row_times)
codes.append(row_codes)
tiers[name] = dict(steps=steps, two_pass=two_pass, estimate_seconds=times,
tiers[name] = dict(steps=steps, two_pass=two_pass, step_cache=step_cache, estimate_seconds=times,
max_seconds=maxima, max_frames=frame_maxima, limit=limits, plan=codes)
default = next((i for i, s in enumerate(sizes) if s['pixels'] == .98), 0)
result = dict(version=1, status='ready',
Expand Down
2 changes: 2 additions & 0 deletions freevideo_engine/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ def main():
generate.add_argument('--seed', type=int, default=2026090901)
generate.add_argument('--two-pass', action=argparse.BooleanOptionalAction, default=True,
help='Sample a smaller canvas, upscale latents and refine (default: on; 8 + 3 steps)')
generate.add_argument('--step-cache', choices=('easycache-av', 'fbcache'),
help='Approximate reuse between sampling steps (disabled by default)')
generate.add_argument('--base-steps', type=int, choices=range(1, 33), metavar='1..32',
help='First-pass steps (default: 8, or explicit profile). Changes may reduce quality.')
generate.add_argument('--refine-steps', type=int, choices=range(1, 32), default=3, metavar='1..31',
Expand Down
9 changes: 8 additions & 1 deletion freevideo_engine/comfy_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ def connect_assets(assets, *, first=None, last=None, reference=None, reference_a
return result


def sampling_plan(report):
"""The report's sampling plan, with the step cache its first pass used (names High and Max)."""
from .effort_forecast import report_step_cache
plan, cache = report.get('sampling_plan'), report_step_cache(report)
return dict(plan, step_cache=cache) if isinstance(plan, dict) and cache else plan


def output_summary(report, relative_video):
video = report.get('video', {})
encoding = report.get('encoding', {})
Expand All @@ -175,7 +182,7 @@ def output_summary(report, relative_video):
gpu_total_bytes=report.get('profile', {}).get('policy', {}).get('hardware', {}).get('vram_total'),
# A small size is sampled larger and scaled back; people see the size they asked for.
geometry=dict(report.get('geometry', {}), **(report.get('output_size') or {})),
sampling_plan=report.get('sampling_plan'),
sampling_plan=sampling_plan(report),
low_memory=bool(report.get('low_memory_mode')),
thermal=thermal_slowdown(worker_gpu(report)),
shared_gpu=shared_gpu_note(worker_gpu(report)),
Expand Down
Loading