From 781e2b06ec18ef23c6893e724065615955fafcd1 Mon Sep 17 00:00:00 2001 From: j Date: Tue, 13 Jan 2026 12:01:56 +0000 Subject: [PATCH 1/5] default to frames --- config.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.jsonc b/config.jsonc index 154f2f3..e5352c6 100644 --- a/config.jsonc +++ b/config.jsonc @@ -1185,7 +1185,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "edits": [], "lists": [] }, - "icons": "posters", + "icons": "frames", "infoIconSize": 256, "item": "", "itemFind": "", From df4410517a5caf957f5d38f95342e0de28c711b8 Mon Sep 17 00:00:00 2001 From: j Date: Tue, 13 Jan 2026 12:02:09 +0000 Subject: [PATCH 2/5] install local urls --- install.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/install.py b/install.py index 20a4484..64a49f1 100755 --- a/install.py +++ b/install.py @@ -105,6 +105,13 @@ if os.path.exists('__init__.py'): new_apps = apps.strip() + ',\n"%s"\n' % name local_settings = local_settings.replace(apps, new_apps) local_settings_changed = True + if 'LOCAL_URLPATTERNS' not in local_settings: + local_settings += ''' +LOCAL_URLPATTERNS = [ + [r'^(?P[A-Z0-9].*)/source(?P[\d\.]+)\.(?P.*)$', 'p_for_power.views.source_frame'], +] +''' + local_settings_changed = True if local_settings_changed: with open(local_settings_py, 'w') as fd: fd.write(local_settings) From f99b48b746e1d49b094eabeecdfd25a921b121c7 Mon Sep 17 00:00:00 2001 From: j Date: Tue, 13 Jan 2026 12:02:33 +0000 Subject: [PATCH 3/5] support png/jpg frames --- views.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 views.py diff --git a/views.py b/views.py new file mode 100644 index 0000000..48638b7 --- /dev/null +++ b/views.py @@ -0,0 +1,52 @@ +import os +import ox + +from django.conf import settings +from django.shortcuts import get_object_or_404 + +import requests + +from oxdjango.http import HttpFileResponse +from item import models +from archive import extract + +def extract_source_frame(self, position, format): + offset = 0 + streams = self.streams() + for stream in streams: + if stream.duration + offset < position: + offset += stream.duration + else: + if not stream.file.is_video or not stream.file.info.get('video'): + return None + position = position - offset + path = None + if stream.file.data: + height = stream.file.info['video'][0]['height'] + video_path = stream.file.data.path + path = os.path.join(settings.MEDIA_ROOT, stream.path(), + 'source-frames', "%dp" % height, "%s.%s" % (position, format)) + if not os.path.exists(path) and stream.media: + extract.frame(video_path, path, position, height, info=stream.file.info) + if not os.path.exists(path): + return None + return path + +def source_frame(request, id, position=None, format="jpg"): + if format not in ("jpg", "png"): + format = "jpg" + content_type = "image/jpeg" if format == "jpg" else "image/png" + item = get_object_or_404(models.Item, public_id=id) + if not item.access(request.user): + return HttpResponseForbidden() + frame = None + if not position: + position = item.poster_frame + else: + position = float(position.replace(',', '.')) + + frame = extract_source_frame(item, position, format) + response = HttpFileResponse(frame, content_type=content_type) + if request.method == 'OPTIONS': + response.allow_access() + return response From b19ba24dbadc6eb1d1e7703514cc5b381b90a4e9 Mon Sep 17 00:00:00 2001 From: j Date: Tue, 13 Jan 2026 12:03:44 +0000 Subject: [PATCH 4/5] render one chapter, smaller files, always use ai for now --- management/commands/render.py | 1 + render.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/management/commands/render.py b/management/commands/render.py index b4a242c..ba477f0 100644 --- a/management/commands/render.py +++ b/management/commands/render.py @@ -16,6 +16,7 @@ class Command(BaseCommand): parser.add_argument('--duration', action='store', dest='duration', default="3600", help='target duration of all fragments in seconds') parser.add_argument('--offset', action='store', dest='offset', default="1024", help='inital offset in pi') parser.add_argument('--no-video', action='store_true', dest='no_video', default=False, help='don\'t render video') + parser.add_argument('--chapter', action='store', dest='chapter', default=None, help='chapter') parser.add_argument('--single-file', action='store_true', dest='single_file', default=False, help='render to single video') parser.add_argument('--keep-audio', action='store_true', dest='keep_audio', default=False, help='keep independent audio tracks') parser.add_argument('--stereo-downmix', action='store_true', dest='stereo_downmix', default=False, help='stereo downmix') diff --git a/render.py b/render.py index 22a2811..6067ea4 100644 --- a/render.py +++ b/render.py @@ -147,16 +147,17 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): clips = all_clips.copy() if length + clip['duration'] > target and length >= vo_min: break - print('%06.3f %06.3f' % (length, clip['duration']), os.path.basename(clip['original'])) length += int(clip['duration'] * fps) / fps # 50/50 original or ai src = clip['original'] audio = clip['original'] - # select ai... + # select ai if we have one if 'ai' in clip: - if chance(seq, 0.5): - src = random_choice(seq, clip['ai'].values(), False) + if True or chance(seq, 0.5): + src = random_choice(seq, list(clip['ai'].values()), False) + + print('%07.3f %07.3f' % (length, clip['duration']), src.split('/')[-2], os.path.basename(clip['original'])) scene['front']['V2'].append({ 'duration': clip['duration'], @@ -348,6 +349,8 @@ def render_all(options): for fragment in fragments: fragment_base += 1 fragment_id = int(fragment['name'].split(' ')[0]) + if options["chapter"] and int(options["chapter"]) != fragment_id: + continue name = fragment['name'].replace(' ', '_') if fragment_id < 10: name = '0' + name @@ -404,7 +407,7 @@ def render_all(options): cmd += ['vn=1'] else: cmd += ['an=1'] - cmd += ['vcodec=libx264', 'x264opts=keyint=1', 'crf=15'] + #cmd += ['vcodec=libx264', 'x264opts=keyint=1', 'crf=15'] subprocess.call(cmd) if ext == '.wav' and timeline.endswith('audio.kdenlive'): cmd = [ From 34fa9e9262a512b977ac09204a1c086393feb389 Mon Sep 17 00:00:00 2001 From: j Date: Tue, 13 Jan 2026 12:04:33 +0000 Subject: [PATCH 5/5] add preview ai/source side by side --- static/js/infoView.p_for_power.js | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/static/js/infoView.p_for_power.js b/static/js/infoView.p_for_power.js index 4ac29a3..ad70b06 100644 --- a/static/js/infoView.p_for_power.js +++ b/static/js/infoView.p_for_power.js @@ -289,6 +289,17 @@ pandora.ui.infoView = function(data, isMixed) { .appendTo($text); } + if (data.type?.join('').includes('ai:')) { + $('
').addClass('ai-preview').appendTo($text); + } + if (data.type?.includes('original')) { + + $('').attr({ + href: 'https://power-video.rmozone.com/#ox/' + data.id, + target: '_blank' + }).html('Open in AI Power Video').appendTo($text); + } + // Duration, Aspect Ratio -------------------------------------------------- if (!isMultiple) { @@ -648,6 +659,7 @@ pandora.ui.infoView = function(data, isMixed) { }; $element.appendTo($text); pandora.api.find(request, function(response) { + let original; response.data.items.forEach(item => { if (item.id != data.id) { var type = item.type ? item.type[0] : 'Unknown' @@ -657,10 +669,44 @@ pandora.ui.infoView = function(data, isMixed) { $element.append( ` ${type}` ) + if (type == 'original') { + original = item.id + } } }) $element.append(`[all]`) pandora.createLinks($element) + if (data.type?.join('').includes('ai:') && original) { + const preview = $text[0].querySelector('.ai-preview') + const src_ai = '480p.mp4' + const src = `/${original}/480p.mp4` + preview.innerHTML = ` + + + + ` + preview.querySelectorAll('video').forEach(video => { + video.addEventListener('play', event => { + preview.querySelectorAll('video').forEach(v => { + if (v != video) { + v.currentTime = video.currentTime + v.play() + } + }) + }) + video.addEventListener('pause', event => { + preview.querySelectorAll('video').forEach(v => { + if (v != video) { + v.pause() + } + }) + }) + }) + } }) }