Skip to content

Commit 19951f8

Browse files
authored
Merge pull request #1393 from Signbank/choose_still
#1392: Generate still images for video, choose modal
2 parents c03bd5d + fb76d14 commit 19951f8

File tree

8 files changed

+289
-8
lines changed

8 files changed

+289
-8
lines changed

signbank/dictionary/adminviews.py

+31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os.path
12

23
from django.views.generic.list import ListView
34
from django.views.generic.detail import DetailView
@@ -7592,3 +7593,33 @@ def annotatedglosslist_ajax_complete(request, annotatedgloss_id):
75927593
'column_values': column_values,
75937594
'USE_REGULAR_EXPRESSIONS': USE_REGULAR_EXPRESSIONS,
75947595
'SHOW_DATASET_INTERFACE_OPTIONS': SHOW_DATASET_INTERFACE_OPTIONS})
7596+
7597+
def fetch_video_stills_for_gloss(request, gloss_id):
7598+
7599+
gloss = Gloss.objects.get(id=gloss_id, archived=False)
7600+
7601+
folder = gloss.idgloss + '-' + gloss_id
7602+
folder = folder.replace(' ', '_')
7603+
temp_location_frames = os.path.join(settings.GLOSS_IMAGE_DIRECTORY, "signbank-thumbnail-frames", folder)
7604+
temp_video_frames_folder = os.path.join(settings.WRITABLE_FOLDER,
7605+
settings.GLOSS_IMAGE_DIRECTORY, "signbank-thumbnail-frames", folder)
7606+
stills = []
7607+
if os.path.exists(temp_video_frames_folder):
7608+
for filename in os.listdir(temp_video_frames_folder):
7609+
still_path = str(os.path.join(temp_location_frames, filename))
7610+
stills.append(still_path)
7611+
sorted_stills = sorted(stills)
7612+
SHOW_DATASET_INTERFACE_OPTIONS = getattr(settings, 'SHOW_DATASET_INTERFACE_OPTIONS', False)
7613+
USE_REGULAR_EXPRESSIONS = getattr(settings, 'USE_REGULAR_EXPRESSIONS', False)
7614+
7615+
selected_datasets = get_selected_datasets_for_user(request.user)
7616+
dataset_languages = get_dataset_languages(selected_datasets)
7617+
7618+
return render(request, 'dictionary/video_stills.html',
7619+
{'focus_gloss': gloss,
7620+
'stills': sorted_stills,
7621+
'dataset_languages': dataset_languages,
7622+
'selected_datasets': selected_datasets,
7623+
'PREFIX_URL': settings.PREFIX_URL,
7624+
'USE_REGULAR_EXPRESSIONS': USE_REGULAR_EXPRESSIONS,
7625+
'SHOW_DATASET_INTERFACE_OPTIONS': SHOW_DATASET_INTERFACE_OPTIONS})

signbank/dictionary/models.py

+10
Original file line numberDiff line numberDiff line change
@@ -2518,6 +2518,16 @@ def create_citation_image(self):
25182518
glossvideo = glossvideos.first()
25192519
glossvideo.make_poster_image()
25202520

2521+
def generate_stills(self):
2522+
from signbank.video.models import GlossVideo
2523+
glossvideos = GlossVideo.objects.filter(gloss=self, glossvideonme=None, glossvideoperspective=None, version=0)
2524+
if not glossvideos:
2525+
msg = ("Gloss::create_stills: no video for gloss %s"
2526+
% self.pk)
2527+
raise ValidationError(msg)
2528+
glossvideo = glossvideos.first()
2529+
glossvideo.make_image_sequence()
2530+
25212531
def published_definitions(self):
25222532
"""Return a query set of just the published definitions for this gloss
25232533
also filter out those fields not in DEFINITION_FIELDS"""

signbank/dictionary/templates/dictionary/gloss_detail.html

+104-3
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,59 @@
202202
<script type="text/javascript" src="{{STATIC_URL}}js/recordscript.js"></script>
203203

204204
<script type='text/javascript'>
205+
$('.quick_createstills').click(function(e)
206+
{
207+
var glossid = $(this).attr('data-glossid');
208+
$.ajax({
209+
url : url + "/dictionary/generate_video_stills_for_gloss/" + glossid,
210+
type: 'POST',
211+
data: { 'csrfmiddlewaretoken': csrf_token },
212+
success : function(data) {
213+
var lookup = "#stills_gloss_video";
214+
$(lookup).empty();
215+
$.ajax({
216+
url : url + "/dictionary/ajax/fetch_video_stills_for_gloss/" + glossid + "/",
217+
datatype: "json",
218+
async: true,
219+
data: { 'csrfmiddlewaretoken': csrf_token },
220+
success : function(result) {
221+
var parsed = $.parseHTML(result);
222+
$(lookup).first().append(result);
223+
}
224+
});
225+
}
226+
});
227+
});
228+
$('.choose_still').click(function(e)
229+
{
230+
var glossid = $(this).attr('data-glossid');
231+
var lookup = "#stills_gloss_video";
232+
var chosen_image = [];
233+
$(lookup).find('input[name="stillimage"]').each(function() {
234+
if (this.checked) {
235+
chosen_image.push(this.value);
236+
}
237+
});
238+
if (!chosen_image.length > 0) {
239+
return;
240+
}
241+
var chosen = chosen_image[0];
242+
$.ajax({
243+
url : url + "/dictionary/save_chosen_still_for_gloss/" + glossid,
244+
type: 'POST',
245+
datatype: "json",
246+
data: { 'csrfmiddlewaretoken': csrf_token,
247+
'imagepath': chosen },
248+
success: function(data) {
249+
var stills_modal = '#stills_modal';
250+
var lookup = "#stills_gloss_video";
251+
$(lookup).empty();
252+
$(stills_modal).modal("hide");
253+
var redirect_url = data.redirect_url;
254+
window.location.replace(redirect_url);
255+
}
256+
});
257+
});
205258
function play_perspective() {
206259
$('#videoplayer_middle').trigger('play');
207260
$('#videoplayer_left').trigger('play');
@@ -458,8 +511,19 @@
458511
textarea:focus {
459512
border: 1px solid red;
460513
}
514+
.radio_stills label:has(+ input[type="radio"]:checked) > img {
515+
outline: 2px solid red;
516+
}
517+
.zoom {
518+
background-color: transparent;
519+
transition: transform .2s; /* Animation */
520+
margin: 0 auto;
521+
}
522+
.zoom:hover {
523+
transform: scale(1.5);
524+
outline: 2px solid red;
525+
}
461526
</style>
462-
463527
{% endblock %}
464528

465529
{% block content %}
@@ -1013,9 +1077,46 @@ <h4>{% trans "Upload New Citation Form Image" %}</h4>
10131077
{% if gloss.has_video and "change_dataset" in dataset_perms %}
10141078
<div class="editform">
10151079
<h4>{% trans "Create Citation Form Image from Current Video" %}</h4>
1016-
<a href="{% url 'dictionary:create_citation_image' gloss.id %}" class="btn btn-primary">Create</a>
1080+
<a href="{% url 'dictionary:create_citation_image' gloss.id %}" class="btn btn-primary">{% trans "Create" %}</a>
1081+
1082+
<button id='choose_still_btn' class='btn btn-primary' style="width:auto;"
1083+
data-toggle='modal'
1084+
data-target='#stills_modal'>{% trans "Choose Still Image" %}</button>
10171085
</div>
1086+
<div class="modal fade" id="stills_modal" tabindex="-1" role="dialog" aria-labelledby="#modalTitleStills" aria-hidden="true">
1087+
<div class="modal-dialog modal-lg left-modal">
1088+
<div class="modal-content">
1089+
<div class='modal-header'>
1090+
<h2 id='modalTitleStills'>{% trans "Choose a Still Image" %}</h2>
1091+
</div>
1092+
<div class='modal-body'>
10181093

1094+
<div style="width:800px;">
1095+
<button id='quick_create_stills'
1096+
class="quick_createstills btn btn-primary" style="width:auto;"
1097+
name='quick_createstills'
1098+
data-glossid='{{gloss.id}}'
1099+
type="submit" >{% trans "Generate Stills" %}
1100+
</button>
1101+
<table class='table table-condensed'>
1102+
<tbody class="tbody tbody-light">
1103+
<tr>
1104+
<td id="stills_gloss_video">
1105+
1106+
</td>
1107+
</tr>
1108+
</tbody>
1109+
</table>
1110+
</div>
1111+
</div>
1112+
<div class="modal-footer">
1113+
<input type="submit" class="choose_still btn btn-primary" id="choose_still" data-glossid="{{gloss.id}}"
1114+
style="width:auto;" value='{% trans "Choose Image" %}'>
1115+
<button type="button" class="btn btn-primary" data-dismiss="modal">{% trans "Dismiss" %}</button>
1116+
</div>
1117+
</div>
1118+
</div>
1119+
</div>
10191120
{% endif %}
10201121

10211122
{% if "change_dataset" in dataset_perms %}
@@ -2244,7 +2345,7 @@ <h2 id='modalTitleBlend'>{% trans "Delete This Blend" %}</h2>
22442345
data-target='#minimalpairs'>{% trans "Minimal Pairs" %}
22452346
</div>
22462347
<div id='minimalpairs' class='collapse'>
2247-
<table class='table table-condensed' id = "header_mp_rows">
2348+
<table class='table table-condensed' id="header_mp_rows">
22482349

22492350
{% if minimalpairs %}
22502351
<tr>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{% load i18n %}
2+
{% load stylesheet %}
3+
{% load annotation_idgloss_translation %}
4+
5+
{% load bootstrap3 %}
6+
{% load tagging_tags %}
7+
8+
{% block extrajs %}
9+
<script type='text/javascript'>
10+
var url = '{{PREFIX_URL}}';
11+
12+
</script>
13+
{% endblock %}
14+
15+
<div id="glossstills_{{focus_gloss.id}}">
16+
{% url 'dictionary:protected_media' '' as protected_media_url %}
17+
{% csrf_token %}
18+
<div class="radio_stills">
19+
{% for imagepath in stills %}
20+
<label class="still_label" for="still_{{forloop.counter}}">
21+
<img class="zoom" src="{{PREFIX_URL}}{{protected_media_url}}{{imagepath}}" width="200">
22+
</label>
23+
<input type="radio" name="stillimage" value="{{imagepath}}" id="still_{{forloop.counter}}">
24+
{% endfor %}
25+
</div>
26+
</div>

signbank/dictionary/urls.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
create_lemma_for_gloss, LemmaUpdateView, SemanticFieldDetailView, SemanticFieldListView, DerivationHistoryDetailView,
1111
DerivationHistoryListView, GlossVideosView, KeywordListView, AnnotatedSentenceDetailView, AnnotatedSentenceListView)
1212

13-
from signbank.dictionary.views import create_citation_image
13+
from signbank.dictionary.views import create_citation_image, generate_video_stills_for_gloss
1414

1515
# These are needed for the urls below
1616
import signbank.dictionary.views
@@ -169,6 +169,7 @@
169169
re_path(r'^ajax/senserow/(?P<sense_id>.*)/$', signbank.dictionary.adminviews.senselist_ajax_complete, name='senselist_ajax_complete'),
170170
re_path(r'^ajax/senselistheader/$', signbank.dictionary.adminviews.senselistheader_ajax, name='senselistheader_ajax'),
171171
re_path(r'^ajax/lemmaglossrow/(?P<gloss_id>.*)/$', signbank.dictionary.adminviews.lemmaglosslist_ajax_complete, name='lemmaglosslist_ajax_complete'),
172+
re_path(r'^ajax/fetch_video_stills_for_gloss/(?P<gloss_id>.*)/$', signbank.dictionary.adminviews.fetch_video_stills_for_gloss, name='fetch_video_stills_for_gloss'),
172173

173174
re_path(r'^ajax/annotatedglosslistheader/$', signbank.dictionary.adminviews.annotatedglosslistheader_ajax,
174175
name='annotatedglosslistheader_ajax'),
@@ -286,6 +287,11 @@
286287
re_path(r'createcitationimage/(?P<pk>\d+)',
287288
permission_required('dictionary.change_gloss')(signbank.dictionary.views.create_citation_image),
288289
name='create_citation_image'),
289-
290+
re_path(r'generate_video_stills_for_gloss/(?P<pk>\d+)',
291+
permission_required('dictionary.change_gloss')(signbank.dictionary.views.generate_video_stills_for_gloss),
292+
name='generate_video_stills_for_gloss'),
293+
re_path(r'save_chosen_still_for_gloss/(?P<pk>\d+)',
294+
permission_required('dictionary.change_gloss')(signbank.dictionary.views.save_chosen_still_for_gloss),
295+
name='save_chosen_still_for_gloss'),
290296
re_path(r'gloss/api/', signbank.dictionary.views.gloss_api_get_sign_name_and_media_info, name='gloss_api_get_info')
291297
]

signbank/dictionary/views.py

+54-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import os.path
2+
13
from django.conf import empty
2-
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest, HttpResponseNotAllowed, Http404
4+
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest, HttpResponseNotAllowed, Http404, JsonResponse
35
from django.shortcuts import render, get_object_or_404, redirect
46
from django.urls import reverse
57
from django.contrib.auth.decorators import login_required
@@ -23,7 +25,7 @@
2325
import signbank.dictionary.forms
2426
from signbank.video.models import GlossVideo, small_appendix, add_small_appendix
2527

26-
from signbank.tools import save_media
28+
from signbank.tools import save_media, get_two_letter_dir
2729
from signbank.tools import get_selected_datasets_for_user, get_default_annotationidglosstranslation, \
2830
get_dataset_languages, \
2931
create_gloss_from_valuedict, compare_valuedict_to_gloss, compare_valuedict_to_lemma, construct_scrollbar, \
@@ -1778,6 +1780,56 @@ def create_citation_image(request, pk):
17781780

17791781
return redirect(url)
17801782

1783+
1784+
def generate_video_stills_for_gloss(request, pk):
1785+
if 'HTTP_REFERER' in request.META:
1786+
url = request.META['HTTP_REFERER']
1787+
else:
1788+
url = '/'
1789+
1790+
gloss = get_object_or_404(Gloss, pk=pk, archived=False)
1791+
try:
1792+
gloss.generate_stills()
1793+
except (PermissionError, OSError) as e:
1794+
feedback_message = getattr(e, 'message', repr(e))
1795+
messages.add_message(request, messages.ERROR, feedback_message)
1796+
1797+
return redirect(url)
1798+
1799+
1800+
def save_chosen_still_for_gloss(request, pk):
1801+
1802+
gloss = get_object_or_404(Gloss, pk=pk, archived=False)
1803+
redirect_url = reverse('dictionary:admin_gloss_view', kwargs={'pk': pk})
1804+
1805+
imagepath = request.POST.get('imagepath', '')
1806+
if not imagepath:
1807+
return JsonResponse({'redirect_url': redirect_url})
1808+
1809+
image_location = os.path.join(WRITABLE_FOLDER, imagepath)
1810+
dataset_folder = gloss.lemma.dataset.acronym
1811+
idgloss = gloss.idgloss
1812+
two_char_folder = get_two_letter_dir(idgloss)
1813+
1814+
vfile_name = idgloss + '-' + str(gloss.id) + '.png'
1815+
still_goal_location = os.path.join(WRITABLE_FOLDER, GLOSS_IMAGE_DIRECTORY, dataset_folder, two_char_folder, vfile_name)
1816+
try:
1817+
if os.path.exists(still_goal_location):
1818+
os.remove(still_goal_location)
1819+
os.rename(image_location, still_goal_location)
1820+
except (PermissionError, OSError) as e:
1821+
feedback_message = getattr(e, 'message', repr(e))
1822+
messages.add_message(request, messages.ERROR, feedback_message)
1823+
1824+
# clean up the unused image files
1825+
from signbank.video.models import GlossVideo
1826+
glossvideo = GlossVideo.objects.filter(gloss=gloss, glossvideonme=None, glossvideoperspective=None, version=0).first()
1827+
if glossvideo:
1828+
glossvideo.delete_image_sequence()
1829+
1830+
return JsonResponse({'redirect_url': redirect_url})
1831+
1832+
17811833
def add_image(request):
17821834

17831835
if 'HTTP_REFERER' in request.META:

signbank/video/convertvideo.py

+47
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,53 @@ def probe_format(file):
121121
return r['inputvideoformat']
122122

123123

124+
def generate_image_sequence(sourcefile):
125+
basename, _ = os.path.splitext(sourcefile.path)
126+
temp_location_frames = os.path.join(settings.WRITABLE_FOLDER,
127+
settings.GLOSS_IMAGE_DIRECTORY, "signbank-thumbnail-frames")
128+
filename, ext = os.path.splitext(os.path.basename(sourcefile.name))
129+
filename = filename.replace(' ', '_')
130+
folder_name, _ = os.path.splitext(filename)
131+
temp_video_frames_folder = os.path.join(temp_location_frames, folder_name)
132+
# Create the necessary subfolder if needed
133+
if not os.path.isdir(temp_location_frames):
134+
os.mkdir(temp_location_frames)
135+
if not os.path.isdir(temp_video_frames_folder):
136+
os.mkdir(temp_video_frames_folder)
137+
else:
138+
# remove old files before generating again
139+
stills_pattern = temp_video_frames_folder + "/*.png"
140+
for f in glob.glob(stills_pattern):
141+
os.remove(f)
142+
(
143+
ffmpeg
144+
.input(sourcefile.path, ss=1)
145+
.filter('fps', fps=15, round='up')
146+
.output("%s/%s-%%04d.png" % (temp_video_frames_folder, folder_name), **{'qscale:v': 2})
147+
.run(quiet=True)
148+
)
149+
stills = []
150+
for filename in os.listdir(temp_video_frames_folder):
151+
still_path = os.path.join(temp_video_frames_folder, filename)
152+
stills.append(still_path)
153+
154+
return stills
155+
156+
157+
def remove_stills(sourcefile):
158+
basename, _ = os.path.splitext(sourcefile.path)
159+
temp_location_frames = os.path.join(settings.WRITABLE_FOLDER,
160+
settings.GLOSS_IMAGE_DIRECTORY, "signbank-thumbnail-frames")
161+
filename, ext = os.path.splitext(os.path.basename(sourcefile.name))
162+
filename = filename.replace(' ', '_')
163+
folder_name, _ = os.path.splitext(filename)
164+
temp_video_frames_folder = os.path.join(temp_location_frames, folder_name)
165+
# remove the temp files
166+
stills_pattern = temp_video_frames_folder+"/*.png"
167+
for f in glob.glob(stills_pattern):
168+
os.remove(f)
169+
170+
124171
def make_thumbnail_video(sourcefile, targetfile):
125172
# this method is not called (need to move temp files to /tmp instead)
126173
# this function also works on source quicktime videos

0 commit comments

Comments
 (0)