Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8f98b5e
Windows int8: medium-size videos on 16 GB or smaller cards no longer …
KBRASK Oct 14, 2026
b15c0c4
Segment-by-segment continuation no longer hits a memory error while a…
KBRASK Oct 14, 2026
7b2de8d
Editing view: reference pictures for a part that starts fresh
KBRASK Oct 14, 2026
da89b7f
Prompt enhancement stops cleanly when turned off or cancelled
KBRASK Oct 14, 2026
44a47f4
Generating no longer fails when a restored workflow leaves the frame …
KBRASK Oct 14, 2026
d74c5c3
Image generation on Windows no longer stalls while the image model loads
KBRASK Oct 14, 2026
7d9a11e
Image generation counts loaded weights that could not be page-locked …
KBRASK Oct 14, 2026
5e1198b
Editor: every Generate press answers at once
KBRASK Oct 14, 2026
f64f689
Editor: reopening keeps it above other views; Delete in the library n…
KBRASK Oct 14, 2026
f902216
Editing view references: limit counts pictures in use; full row keeps…
KBRASK Oct 14, 2026
9072b72
Image model check reads the file quickly on slow disks instead of sta…
KBRASK Oct 14, 2026
e8f0f00
Mac: text encoder memory fits the machine; compatibility settings hid…
KBRASK Oct 14, 2026
c190d9c
Editor: imported clips can be extended like generated ones
KBRASK Oct 14, 2026
ff01f24
Editor: files dropped on the timeline land where they are dropped
KBRASK Oct 14, 2026
2ba0047
Other model folders are saved and used across setup and launch
KBRASK Oct 14, 2026
71bd6d6
Put each saved model folder that was not found on its own line
KBRASK Oct 14, 2026
f203524
Edge model downloads use the ModelScope domestic site; image models a…
KBRASK Oct 14, 2026
ba9eeb9
Image editor protects unsaved edits and explains its actions
KBRASK Oct 14, 2026
6484947
Image worker falls back to standard input when the private handle can…
KBRASK Oct 14, 2026
a96d115
Video models are released after each generation unless the next part …
KBRASK Oct 14, 2026
5d29588
Editor: queued parts survive a reload and fall back to drafts when ge…
KBRASK Oct 14, 2026
0f92d20
Release 0.4.4
KBRASK Oct 14, 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
29 changes: 26 additions & 3 deletions freevideo_engine/agent/cast.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,23 @@ def _check(record, stem):
or type(card['revision']) is not int or card['revision'] < 1
or not isinstance(card['name'], str) or not isinstance(card['description'], (str, type(None)))):
raise ValueError('Invalid card identity')
if set(card['edited_by_user']) != set(EDITABLE) or not all(type(v) is bool for v in card['edited_by_user'].values()):
marks = card.get('edited_by_user', {})
if not isinstance(marks, dict):
raise ValueError('Invalid edit marks')
normalized = {k: marks.get(k, False) for k in EDITABLE}
if not all(type(v) is bool for v in normalized.values()):
raise ValueError('Invalid edit marks')
if marks != normalized:
logging.getLogger(__name__).warning('Normalized edit marks on a readable cast card')
card['edited_by_user'] = normalized
unknown_labels = False
for key, limit in (('images', MAX_IMAGES), ('candidates', MAX_CANDIDATES)):
if not isinstance(card[key], list) or len(card[key]) > limit:
raise ValueError('Invalid pictures')
for row in card[key]:
if row['label'] not in LABELS or not isinstance(row['media']['file'], str):
if not isinstance(row['label'], str) or not isinstance(row['media']['file'], str):
raise ValueError('Invalid picture')
unknown_labels |= row['label'] not in LABELS
if card['voice'] is not None:
if not isinstance(card['voice']['clip']['file'], str) or not (
isinstance(card['voice']['media'].get('file'), str) or isinstance(card['voice']['media'].get('library'), str)):
Expand All @@ -145,6 +154,8 @@ def _check(record, stem):
raise ValueError('Invalid film list')
record['receipts'] = operation_receipts.load(record['receipts'])
json.dumps(record, allow_nan=False)
if unknown_labels:
logging.getLogger(__name__).warning('Kept unknown picture labels on a readable cast card')


def _revision(body, header):
Expand Down Expand Up @@ -363,7 +374,19 @@ def list(self, *, q=None, kind=None, origin=None, production=None, limit=50):

def update(self, identity, value, *, by='builtin', ref=None, revision=None, local=False, client=None):
shape = schema.cast_schema('update', local=local)
schema.validate(shape, value)
shape['properties']['images']['items']['properties']['label'] = schema.enum(LABELS)
checked = deepcopy(value)
with self.changed:
record = self.records.get(identity) if isinstance(identity, str) else None
if record is not None and isinstance(checked, dict) and isinstance(checked.get('images'), list):
known = {(row['media']['file'], row['label']) for key in ('images', 'candidates')
for row in record['card'][key] if row['label'] not in LABELS}
for row in checked['images']:
if isinstance(row, dict) and isinstance(row.get('label'), str):
if (_plain(row.get('media')), row['label']) in known:
# Let callers return a saved label unchanged; new labels still use the request schema.
row.pop('label')
schema.validate(shape, checked)
ref = ref if ref is not None else value.get('ref')
value = {k: v for k, v in value.items() if k != 'ref'}
expected = _revision(value.pop('revision', None), revision)
Expand Down
11 changes: 7 additions & 4 deletions freevideo_engine/agent/editor_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,15 @@
};
try {
const record = model.parse(request.project);
const generation = __load('editor_generation');
if (request.mode === 'request') {
const value = model.generationRequest(record, request.ids, {text: request.text, paths: request.paths});
return JSON.stringify({ok: true, blocks: value.blocks.map(({before, ...block}) => block)});
// Parts that continue finished footage are made at its shape, as on the page.
const follow = generation.continuedSize(model.generationBefore(record, request.ids), new Map(Object.entries(request.sizes || {})));
return JSON.stringify({ok: true, blocks: value.blocks.map(({before, ...block}) => block), size: follow});
}
const store = __load('editor_store'), generation = __load('editor_generation');
const size = generation.generationSize(record), target = request.target, words = request.text;
const store = __load('editor_store');
const size = request.size || generation.generationSize(record), target = request.target, words = request.text;
const reply = request.capability, ready = reply && (!reply.status || reply.status === 'ready');
const limits = ready ? store.capabilityPoints(reply, {...record, ...size})
: {maxSeconds: 15, maxFollowSeconds: 15, maxRequestFrames: null};
Expand Down Expand Up @@ -354,7 +357,7 @@ def batch(self, project, operations):
return self._entry('__batch', dict(project=project, operations=operations))

def generation(self, **request):
"""mode='plan' (project, target, text, capability) or mode='request' (project, ids, text, paths)."""
"""mode='plan' (project, target, text, capability[, size]) or mode='request' (project, ids, text, paths, sizes)."""
return self._entry('__generation', request)

def deliver(self, **request):
Expand Down
13 changes: 8 additions & 5 deletions freevideo_engine/agent/productions.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,8 @@ def load(self):
raise ValueError('Invalid execution receipts')
record['receipts'] = operation_receipts.load(record['receipts'])
json.dumps(record, allow_nan=False)
checked = production.plan(dict(screenplay=state['screenplay'],
takes_per_shot=state['takes_per_shot'], director=state['director']))
if [s['id'] for s in state['shots']] != [s['id'] for s in checked['preview']['shots']]:
raise ValueError('Invalid shot receipts')
request = dict(screenplay=state['screenplay'],
takes_per_shot=state['takes_per_shot'], director=state['director'])
for ref, child in record['children'].items():
schema.validate(schema.REF, ref)
if child['status'] not in DONE:
Expand All @@ -174,10 +172,15 @@ def load(self):
raise ValueError('Invalid submission marker')
production.view(state)
try:
checked = production.plan(request)
if [s['id'] for s in state['shots']] != [s['id'] for s in checked['preview']['shots']]:
raise JobError('internal_error', 'Saved shots differ from the current plan')
production.advance(deepcopy(state), deepcopy(record['children']), now=state['updated_at'])
except production.ProductionError as error:
except (production.ProductionError, JobError) as error:
# A readable record that cannot advance yet is kept, never renamed away.
record['stalled'] = self._stalled(error)
logging.getLogger(__name__).warning('Kept a readable production receipt stalled (%s)',
record['stalled']['code'])
self.records[state['id']] = record
except (ValueError, KeyError, TypeError, AttributeError, IndexError, RecursionError, OSError):
path.replace(path.with_name(path.name + '.bad-' + uuid.uuid4().hex))
Expand Down
16 changes: 12 additions & 4 deletions freevideo_engine/agent/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,18 @@ def generate(self, identity, value, *, jobs, capability=None, ref=None, client=N
if any(info['missing'] or not info.get('path') for info in found.values()):
raise ProjectError('source_missing', 'The part this generation continues from is not available.')
request = rules.generation(mode='request', project=plan['project'], ids=plan['ids'], text=text,
paths={key: info['path'] for key, info in found.items()})
paths={key: info['path'] for key, info in found.items()},
sizes={key: dict(width=info['width'], height=info['height']) for key, info in found.items()})
if not request['ok']:
self._rejected(request['error'], stored, '/target', target)
limit = plan['max_request_frames']
size, limit = plan['size'], plan['max_request_frames']
if request.get('size') and request['size'] != size:
# A part continuing footage of another size has that size's limit, as on the page.
size = request['size']
reply = capability(size) if capability is not None else None
if isinstance(reply, dict) and reply.get('status', 'ready') == 'ready':
limit = rules.generation(mode='plan', project=stored, target=target, text=text, capability=reply,
size=size).get('max_request_frames')
longest = max(block['frames'] for block in request['blocks'])
if limit is not None and longest > limit:
raise _invalid('/target', 'parts of at most %d frames at this size and quality; choose a shorter range'
Expand All @@ -338,8 +346,8 @@ def generate(self, identity, value, *, jobs, capability=None, ref=None, client=N
prompt = text.strip() or next((b['text'] for b in blocks if b['text'].strip()), '') or 'Continue the video.'
frames = sum(block['frames'] for block in blocks)
spec = dict({k: v for k, v in settings.items() if k in ('quality', 'sampling', 'loras', 'force_regenerate')},
kind='generate', prompt=prompt, seed=seed, width=plan['size']['width'],
height=plan['size']['height'], seconds=min(60., max(1.625, frames / 24)),
kind='generate', prompt=prompt, seed=seed, width=size['width'],
height=size['height'], seconds=min(60., max(1.625, frames / 24)),
label='Editing project ' + identity)
saved = self._save(plan['project']) # The reservation: the page's revision check orders it.
editor = dict(project=identity, ids=plan['ids'], blocks=blocks, revision=saved['revision'])
Expand Down
3 changes: 2 additions & 1 deletion freevideo_engine/agent/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ def cast_schema(name, *, local=False):
if name == 'create':
return obj(dict(kind=enum(('character', 'setting'), default='character'), **fields, ref=REF), ('name',))
if name == 'update':
fields['images']['items']['properties']['label'] = {'type': 'string'}
return obj(dict(fields, revision=number(1, integer=True), ref=REF))
if name == 'delete':
return obj(dict(revision=number(1, integer=True), ref=REF))
Expand All @@ -489,7 +490,7 @@ def cast_schema(name, *, local=False):
def cast_view():
picture = obj(dict(media=obj(dict(file=FILE, url=string(2048)), ('file', 'url')),
by=enum(('user', 'agent', 'builtin')), from_job=JOB, generated={'type': 'boolean'},
label=enum(LABELS), label_guess={'type': 'boolean'}), ('media', 'by', 'label'))
label={'type': 'string'}, label_guess={'type': 'boolean'}), ('media', 'by', 'label'))
generating = obj(dict(job=JOB, face_job=nullable(JOB), jobs=array(JOB, 1, 5),
source=enum(('image', 'video')), replace={'type': 'boolean'}, at=string(),
by=enum(('user', 'agent', 'builtin'))), ('job', 'face_job', 'replace', 'at', 'by'))
Expand Down
Loading