From 4b3b225a2c36b79bb077c384c4b42747a4bee72d Mon Sep 17 00:00:00 2001 From: j Date: Sun, 25 Jan 2026 20:18:10 +0100 Subject: [PATCH 1/4] render fixes --- render.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/render.py b/render.py index b08de77..57925ef 100644 --- a/render.py +++ b/render.py @@ -84,7 +84,7 @@ def compose(clips, fragment, target=150, base=1024, voice_over=None, options=Non selected_clips = [] tags = [] - while selected_clips_length < target: + while selected_clips_length < target * 1.1: if not tags: tags = fragment["tags"].copy() tag = random_choice(seq, tags, pop=True) @@ -132,18 +132,18 @@ def compose(clips, fragment, target=150, base=1024, voice_over=None, options=Non next_length = length + clip['duration'] if target - next_length < -target*0.1: break - length += int(clip['duration'] * fps) / fps + clip_duration = int(clip['duration'] * fps) / fps + length += clip_duration # 50/50 source or ai src = clip['source'] audio = clip['source'] # select ai if we have one - if 'ai' in clip: - if clip["use_ai"]: - src = random_choice(seq, list(clip['ai'].values()), False) + if 'ai' in clip and clip.get("use_ai"): + src = random_choice(seq, list(clip['ai'].values()), False) print('%07.3f-%07.3f %07.3f %s (%s)' % ( - length-clip['duration'], + length-clip_duration, length, clip['duration'], os.path.basename(clip['source']), @@ -196,6 +196,9 @@ def compose(clips, fragment, target=150, base=1024, voice_over=None, options=Non if not clips: print("not enough clips, also consider last clip") clips = all_clips.copy() + for clip in clips: + if "ai" in clip: + clip["use_ai"] = True scene_duration = int(get_scene_duration(scene) * fps) voice_overs = [] From 73e22ed68572b8df3e185ef736e38f43bfb44cdc Mon Sep 17 00:00:00 2001 From: j Date: Mon, 26 Jan 2026 09:23:29 +0100 Subject: [PATCH 2/4] add character to documents --- config.jsonc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config.jsonc b/config.jsonc index aee6925..56f41a1 100644 --- a/config.jsonc +++ b/config.jsonc @@ -265,6 +265,15 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "find": true, "sort": true }, + { + "id": "character", + "title": "Character", + "type": "string", + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, { "id": "prompt", "title": "Prompt", From ccbde021071cc335a6b697562c645434607c9b01 Mon Sep 17 00:00:00 2001 From: j Date: Mon, 26 Jan 2026 09:23:47 +0100 Subject: [PATCH 3/4] multi character --- generate.py | 198 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 134 insertions(+), 64 deletions(-) diff --git a/generate.py b/generate.py index 9dff58d..74d531c 100644 --- a/generate.py +++ b/generate.py @@ -16,6 +16,7 @@ from django.conf import settings from item.models import Item from document.models import Document from archive.models import File, Stream +import itemlist.models os.environ["FAL_KEY"] = settings.FAL_KEY @@ -34,7 +35,7 @@ def public_url(path): def public_document_url(document): url = "%sdocuments/%s/source.%s?token=%s" % ( settings.PUBLIC_URL, - ox.toAZ(document.id), + document.get_id(), document.extension, settings.PUBLIC_TOKEN, ) @@ -89,6 +90,64 @@ def trim_video(src, dst, frames, start0=False): out.release() cap.release() +def make_single_character_image(character): + character = get_character_document(character, type='Character') + character_url = public_document_url(character) + data = { + "model": "seedream-4-5-251128", + "size": "2K", + "watermark": False, + 'image': character_url, + "prompt": "character from image 1 is standing straight up, full body portrait from head to toe. face, clothing, skin are photo realistic, camera facing straight at character. background is white" + } + url = bytedance_image_generation(data) + extension = url.split(".")[-1].split("?")[0] + if extension == "jpeg": + extension = "jpg" + file = Document(user=character.user) + file.data["title"] = character.data['title'].replace('Character', 'Single Character') + file.extension = extension + file.width = -1 + file.pages = -1 + file.uploading = True + file.save() + file.uploading = True + name = "data.%s" % file.extension + file.file.name = file.path(name) + ox.net.save_url(url, file.file.path, overwrite=True) + file.get_info() + file.get_ratio() + file.oshash = ox.oshash(file.file.path) + file.save() + file.update_sort() + return file + +def make_single_character_image_flux(character): + character = get_character_document(character, type='Character') + character_url = public_document_url(character) + prompt = 'character from @image 1 is standing straight up, full body portrait from head to toe. face, clothing, skin are photo realistic, camera facing straight at character. background is white' + url = flux_edit_image([character_url], prompt) + extension = url.split(".")[-1].split("?")[0] + if extension == "jpeg": + extension = "jpg" + file = Document(user=character.user) + file.data["title"] = character.data['title'].replace('Character', 'FLUX Single Character') + file.extension = extension + file.width = -1 + file.pages = -1 + file.uploading = True + file.save() + file.uploading = True + name = "data.%s" % file.extension + file.file.name = file.path(name) + ox.net.save_url(url, file.file.path, overwrite=True) + file.get_info() + file.get_ratio() + file.oshash = ox.oshash(file.file.path) + file.save() + file.update_sort() + return file + def bytedance_task(data): url = "https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks" @@ -394,6 +453,10 @@ def process_frame(item, prompt, character=None, position=0, seed=None): img.update_find() return img +def get_character_document(character, type="Single Character"): + if character in ("P1", "P2", "P3", "P4", "P5"): + return Document.objects.get(data__title=type + " " + character) + return character """ REPLACE_CHARACTER_PROMPT = "Replace the foreground character in image 1 with the character in image 2, keep the posture, clothing, background, light, atmosphere from image 1, but take the facial features and personality from image 2. Make sure the size of the character is adjusted since the new character is a child and make sure the size of the head matches the body. The quality of the image should be the same between foreground and background, adjust the quality of the character to match the background. Use the style of image 1 for the character: if image 1 is a photo make the character a real person, if image 1 is a drawing make the character a drawn character, if image 1 is a comic use a comic character and so on" @@ -410,8 +473,8 @@ def fal_replace_character(item, character, position=0): ) if character == "P5": prompt = prompt.replace("child", "teenager") - if character in ("P1", "P2", "P3", "P4", "P5"): - character = Document.objects.get(data__title="Character " + character) + + character = get_character_document(character) if isinstance(character, Document): character = public_document_url(character) image = public_frame_url(item, position) @@ -423,6 +486,11 @@ def fal_replace_character(item, character, position=0): img.data["prompt"] = prompt img.data["source"] = item.public_id img.data["source"] += " " + character.split("?")[0] + if isinstance(character, Document): + img.data["character"] = character.get_id() + else: + img.data["character"] = character + img.data["position"] = position print(img, img.data) img.save() img.update_sort() @@ -434,11 +502,19 @@ def replace_character(item, character, position=0, seed=None): prompt = REPLACE_CHARACTER_PROMPT if character == "P5": prompt = prompt.replace("child", "teenager") - if character in ("P1", "P2", "P3", "P4", "P5"): - character = public_document_url( - Document.objects.get(data__title="Character " + character) - ) - return process_frame(item, prompt, character, position, seed=seed) + character = get_character_document(character) + if isinstance(character, Document): + character_url = public_document_url(character) + else: + character_url = character + frame = process_frame(item, prompt, character_url, position, seed=seed) + if isinstance(character, Document): + frame.data["character"] = character.get_id() + else: + frame.data["character"] = character + frame.data["position"] = position + frame.save() + return frame def kling_lipsync(audio_item, video_item): @@ -474,10 +550,15 @@ def kling_v2v_reference(item, character, keep=False): character = public_document_url( Document.objects.get(data__title="Character " + character) ) + character = get_character_document(character) + if isinstance(character, Document): + character_url = public_document_url(character) + else: + character_url = character video_url = public_video_url(item) prompt = "Replace the main character in @Video1 with the character from the reference images, adjust the style of the character to match the style of the video" model = "fal-ai/kling-video/o1/video-to-video/reference" - prompt_hash = hashlib.sha1((prompt + character).encode()).hexdigest() + prompt_hash = hashlib.sha1((prompt + character_url).encode()).hexdigest() output = "/srv/pandora/static/power/cache/%s_%s/ai.mp4" % ( item.public_id, prompt_hash, @@ -499,7 +580,7 @@ def kling_v2v_reference(item, character, keep=False): "keep_audio": False, "aspect_ratio": "16:9", "video_url": video_url, - "image_urls": [image_url], + "image_urls": [character_url], "duration": str(duration) } ''' @@ -624,11 +705,14 @@ def reshoot_item(item, extra_prompt=None, first_frame=None, keep=False, prompt=N if isinstance(item, str): item = Item.objects.get(public_id=item) if isinstance(first_frame, Document): - first_frame = public_document_url(first_frame) + first_frame_url = public_document_url(first_frame) + else: + first_frame_url = first_frame duration = item.sort.duration frames = int(duration * 24) + neutral = first_frame is not None if prompt is None: - prompt = describe_item(item, first_frame is not None) + prompt = describe_item(item, neutral) if extra_prompt: prompt += " " + extra_prompt prompt_hash = hashlib.sha1((prompt).encode()).hexdigest() @@ -637,7 +721,7 @@ def reshoot_item(item, extra_prompt=None, first_frame=None, keep=False, prompt=N prompt_hash, ) if first_frame: - status = i2v_bytedance(first_frame, prompt, duration, output) + status = i2v_bytedance(first_frame_url, prompt, duration, output) else: status = t2v_bytedance(prompt, duration, output) @@ -654,9 +738,11 @@ def reshoot_item(item, extra_prompt=None, first_frame=None, keep=False, prompt=N ai.data["model"] = status["model"] ai.data["seed"] = status["seed"] if first_frame: - ai.data["firstframe"] = first_frame.split("?")[0] if isinstance(first_frame, Document): first_frame.add(ai) + ai.data["firstframe"] = first_frame.get_id() + else: + ai.data["firstframe"] = first_frame.split("?")[0] ai.save() if not keep: shutil.rmtree(os.path.dirname(output)) @@ -719,7 +805,7 @@ def reshoot_item_segments(item, character, keep=False): ai = add_ai_variant(item, joined_output, "ai:0:reshoot-firstframe") prompt = "\n\n".join(prompts) ai.data["prompt"] = ox.escape_html(prompt) - ai.data["firstframe"] = " ".join([ox.toAZ(ff.id) for ff in first_frames]) + ai.data["firstframe"] = " ".join([ff.get_id() for ff in first_frames]) ai.data["model"] = status["model"] ai.data["seed"] = seed ai.save() @@ -1137,22 +1223,6 @@ def add_tag(item, tag): item.data['tags'].append(tag) item.save() -def process_motion_firstframe(character="P1", keep=False): - l = itemlist.models.List.objects.get(name='Motion-Firstframe') - for i in l.items.all(): - ai = Item.objects.filter(data__type__icontains='ai').filter(data__title=i.data['title']) - if ai.exists() or 'ai-failed' in i.data.get('tags', []): - print('>> skip', i) - continue - print(i) - try: - replace_character_motion_control(i, character, keep=keep) - except: - i.refresh_from_db() - add_tag(i, 'ai-failed') - print('>> failed', i) - - def extract_firstframe(character='P1'): for item in Item.objects.filter(data__type__icontains="source"): if 'ai-failed' in item.data.get('tags', []): @@ -1165,43 +1235,43 @@ def extract_firstframe(character='P1'): item.refresh_from_db() add_tag(item, 'ai-failed') -def process_reshoot_firstframe(): +def process_reshoot_firstframe(character='P1'): + position = 0 l = itemlist.models.List.objects.get(name='Reshoot-Firstframe') - for i in l.items.all(): - if i.sort.duration > 30: continue - if i.public_id == 'HZ': continue - if i.documents.all().count(): - ai = Item.objects.filter(data__type__icontains='ai').filter(data__title=i.data['title']) - if ai.exists() or 'ai-failed' in i.data.get('tags', []): - print('>> skip', i) - continue - first_frame = i.documents.all().order_by('-created').first() + for item in l.items.all(): + if 'ai-failed' in item.data.get('tags', []): + print('>> skip', item) + continue + if item.sort.duration > 30: + reshoot_item_segments(item, character) + else: + cid = get_character_document(character).get_id() + first_frame = item.documents.filter( + data__character=cid, data__position=position + ).order_by('-created').first() if not first_frame: - first_frame = replace_character(i, 'P1', 0) - print(i, first_frame, i.documents.all().count()) + first_frame = replace_character(item, character, position) + if first_frame.items.filter(data__type__icontains='ai:').exists(): + continue + print(item, first_frame) try: - reshoot_item(i, first_frame=first_frame) + reshoot_item(item, first_frame=first_frame) except: - add_tag(i, 'ai-failed') - print('>> failed', i) + add_tag(item, 'ai-failed') + print('>> failed', item) -def process_motion_firstframe(): +def process_motion_firstframe(character="P1", keep=False): l = itemlist.models.List.objects.get(name='Motion-Firstframe') - for i in l.items.all(): - if i.sort.duration > 30: continue - if i.public_id == 'HZ': continue - if i.documents.all().count(): - ai = Item.objects.filter(data__type__icontains='ai').filter(data__title=i.data['title']) - if ai.exists() or 'ai-failed' in i.data.get('tags', []): - print('>> skip', i) - continue - first_frame = i.documents.all().order_by('-created').first() - if not first_frame: - first_frame = replace_character(i, 'P1', 0) - print(i, first_frame, i.documents.all().count()) - try: - replace_character_motion_control(i, first_frame) - except: - add_tag(i, 'ai-failed') - print('>> failed', i) + for item in l.items.all(): + ai = Item.objects.filter(data__type__icontains='ai').filter(data__title=item.data['title']) + if ai.exists() or 'ai-failed' in item.data.get('tags', []): + print('>> skip', item) + continue + print(i) + try: + replace_character_motion_control(item, character, keep=keep) + except: + item.refresh_from_db() + add_tag(item, 'ai-failed') + print('>> failed', item) From e3ae1367d656ac42a165091cc699bd71a55201a6 Mon Sep 17 00:00:00 2001 From: j Date: Mon, 26 Jan 2026 09:23:57 +0100 Subject: [PATCH 4/4] add backup timer --- db/reload.sh | 7 +++++++ db/sync_pandora.sh | 5 +++++ db/update.sh | 4 ++++ etc/systemd/system/pandora-backup-db.service | 8 ++++++++ etc/systemd/system/pandora-backup-db.timer | 9 +++++++++ 5 files changed, 33 insertions(+) create mode 100755 db/reload.sh create mode 100755 db/sync_pandora.sh create mode 100755 db/update.sh create mode 100644 etc/systemd/system/pandora-backup-db.service create mode 100644 etc/systemd/system/pandora-backup-db.timer diff --git a/db/reload.sh b/db/reload.sh new file mode 100755 index 0000000..e14ebfe --- /dev/null +++ b/db/reload.sh @@ -0,0 +1,7 @@ +#!/bin/sh +pandoractl stop +sudo -H -u postgres dropdb pandora +sudo -H -u postgres createdb -T template0 --locale=C --encoding=UTF8 -O pandora pandora +zcat /srv/pandora/data/db/latest.psql.gz | pandoractl manage dbshell +pandoractl start + diff --git a/db/sync_pandora.sh b/db/sync_pandora.sh new file mode 100755 index 0000000..b84d4c8 --- /dev/null +++ b/db/sync_pandora.sh @@ -0,0 +1,5 @@ +#!/bin/bash +rsync -avP time:/srv/pandora/data/ /srv/pandora/data/ +bash /srv/pandora/data/db/reload.sh +rsync -avP time:/srv/pandora/data/ /srv/pandora/data/ --delete +pandoractl manage generate_clips diff --git a/db/update.sh b/db/update.sh new file mode 100755 index 0000000..7fea261 --- /dev/null +++ b/db/update.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd /srv/pandora/data/db +pg_dump pandora | gzip > pandora-`date -I`.psql.gz +ln -sf pandora-`date -I`.psql.gz latest.psql.gz diff --git a/etc/systemd/system/pandora-backup-db.service b/etc/systemd/system/pandora-backup-db.service new file mode 100644 index 0000000..c8dc18b --- /dev/null +++ b/etc/systemd/system/pandora-backup-db.service @@ -0,0 +1,8 @@ +Unit] +Description=Backup pandora database + +[Service] +Type=oneshot +User=pandora +Group=pandora +ExecStart=/srv/pandora/db/update.sh diff --git a/etc/systemd/system/pandora-backup-db.timer b/etc/systemd/system/pandora-backup-db.timer new file mode 100644 index 0000000..a5e7805 --- /dev/null +++ b/etc/systemd/system/pandora-backup-db.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Backup pandora database + +[Timer] +OnCalendar=*-*-* 4:00 +Persistent=true + +[Install] +WantedBy=timers.target