Compare commits

...

5 commits

Author SHA1 Message Date
j
34fa9e9262 add preview ai/source side by side 2026-01-13 12:04:33 +00:00
j
b19ba24dba render one chapter, smaller files, always use ai for now 2026-01-13 12:03:44 +00:00
j
f99b48b746 support png/jpg frames 2026-01-13 12:02:33 +00:00
j
df4410517a install local urls 2026-01-13 12:02:09 +00:00
j
781e2b06ec default to frames 2026-01-13 12:01:56 +00:00
6 changed files with 115 additions and 6 deletions

View file

@ -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": "",

View file

@ -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<id>[A-Z0-9].*)/source(?P<position>[\d\.]+)\.(?P<format>.*)$', '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)

View file

@ -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')

View file

@ -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 = [

View file

@ -289,6 +289,17 @@ pandora.ui.infoView = function(data, isMixed) {
.appendTo($text);
}
if (data.type?.join('').includes('ai:')) {
$('<div>').addClass('ai-preview').appendTo($text);
}
if (data.type?.includes('original')) {
$('<a>').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(
` <a href="/${item.id}/info">${type}</a>`
)
if (type == 'original') {
original = item.id
}
}
})
$element.append(`[<a href="/grid/title/title=${pandora.escapeQueryValue(title)}">all</a>]`)
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 = `
<video src="${src}" controls loop></video>
<video src="${src_ai}" loop></video>
<style>
.ai-preview video {
width: 33%;
}
</style>
`
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()
}
})
})
})
}
})
}

52
views.py Normal file
View file

@ -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