mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-18 21:09:42 -05:00
feat: whisper STT and record screen (#1363)
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block headtitle %}Add new media - {{PORTAL_NAME}}{% endblock headtitle %}
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block externallinks %}
|
||||
{% if LOAD_FROM_CDN %}
|
||||
@@ -19,6 +22,8 @@
|
||||
{%endblock topimports %}
|
||||
|
||||
{% block innercontent %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
|
||||
{% if can_add %}
|
||||
@@ -26,7 +31,7 @@
|
||||
<div class="media-uploader-wrap">
|
||||
<div class="media-uploader-top-wrap">
|
||||
<div class="media-uploader-top-left-wrap">
|
||||
<h1>Upload media files</h1>
|
||||
<h1>{{ "Upload media" | custom_translate:LANGUAGE_CODE}}</h1>
|
||||
</div>
|
||||
<div class="media-uploader-top-right-wrap"> </div>
|
||||
</div>
|
||||
@@ -38,16 +43,19 @@
|
||||
<div class="media-drag-drop-content">
|
||||
<div class="media-drag-drop-content-inner">
|
||||
<span><i class="material-icons">cloud_upload</i></span>
|
||||
<span>Drag and drop files</span>
|
||||
<span>or</span>
|
||||
<span>{{ "Drag and drop files" | custom_translate:LANGUAGE_CODE}}</span>
|
||||
<span>{{ "or" | custom_translate:LANGUAGE_CODE}}</span>
|
||||
<span class="browse-files-btn-wrap">
|
||||
<span class="qq-upload-button-selector">Browse your files</span>
|
||||
<span class="qq-upload-button-selector">{{ "Browse your files" | custom_translate:LANGUAGE_CODE}}</span>
|
||||
</span>
|
||||
|
||||
<div class="qq-upload-drop-area-selector media-dropzone" qq-hide-dropzone>
|
||||
<span class="qq-upload-drop-area-text-selector"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,7 +84,7 @@
|
||||
<span class="filename-edit qq-edit-filename-icon-selector" aria-label="Edit filename">Edit filename <i class="material-icons">create</i></span>
|
||||
<button type="button" class="delete-media-upload-item qq-upload-delete-selector" aria-label="Delete">Delete <i class="material-icons">delete</i></button>
|
||||
<button type="button" class="cancel-media-upload-item qq-upload-cancel-selector" aria-label="Cancel">Cancel <i class="material-icons">cancel</i></button>
|
||||
<a href="#" class="view-uploaded-media-link qq-hide" target="_blank">View media <i class="material-icons">open_in_new</i></a>
|
||||
<a href="#" class="view-uploaded-media-link qq-hide" target="_blank">{{ "View media" | custom_translate:LANGUAGE_CODE}}<i class="material-icons">open_in_new</i></a>
|
||||
</div>
|
||||
<div class="media-upload-item-bottom-actions">
|
||||
<button type="button" class="continue-media-upload-item qq-upload-continue-selector" aria-label="Continue"><i class="material-icons">play_circle_outline</i> Continue</button>
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block headtitle %}Add subtitle - {{PORTAL_NAME}}{% endblock headtitle %}
|
||||
|
||||
{% block innercontent %}
|
||||
{% include "cms/media_nav.html" with active_tab="subtitles" %}
|
||||
|
||||
<div class="user-action-form-wrap">
|
||||
<div class="user-action-form-inner">
|
||||
<h1>Add subtitle</h1>
|
||||
Media: <a href="{{media.get_absolute_url}}">{{media.title}}</a>
|
||||
|
||||
<form enctype="multipart/form-data" action="" method="post" class="post-form">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="primaryAction" type="submit">Add</button>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if subtitles %}
|
||||
<h3>View/Edit Existing Subtitles</h3>
|
||||
|
||||
{% if subtitles %}
|
||||
<div class="user-action-form-wrap">
|
||||
<div class="user-action-form-inner">
|
||||
<h3>Existing Subtitles</h3>
|
||||
<ul>
|
||||
{% for subtitle in subtitles %}
|
||||
<li><a href="{{subtitle.url}}">{{subtitle.language.title}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if whisper_form %}
|
||||
<div class="user-action-form-wrap">
|
||||
<div class="user-action-form-inner">
|
||||
<h3 style="display: flex; align-items: center; gap: 0.25rem;">Request Automatic Tranascription
|
||||
<span title="This is Automatic Transcription using a Whisper model that is loaded locally" style="cursor: help;">
|
||||
<i class="material-icons">info_outline</i>
|
||||
</span>
|
||||
</h3>
|
||||
<form enctype="multipart/form-data" action="" method="post" class="post-form">
|
||||
{% csrf_token %}
|
||||
{% crispy whisper_form %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock innercontent %}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
{% load custom_filters %}
|
||||
{% load i18n %}
|
||||
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<div class="media-edit-nav" style="background-color: #f0f0f0; padding: 10px 0; margin-bottom: 20px;">
|
||||
<ul style="list-style: none; display: flex; justify-content: space-around; margin: 0; padding: 0;">
|
||||
<li style="display: inline-block;">
|
||||
<a href="{% url 'edit_media' %}?m={{media_object.friendly_token}}"
|
||||
style="text-decoration: none; {% if active_tab == 'metadata' %}font-weight: bold; color: #333; padding-bottom: 3px; border-bottom: 2px solid #333;{% else %}color: #666;{% endif %}">
|
||||
Metadata
|
||||
{{ "Metadata" | custom_translate:LANGUAGE_CODE}}
|
||||
</a>
|
||||
</li>
|
||||
{% if media_object.media_type == 'video' %}
|
||||
<li style="display: inline-block;">
|
||||
<a href="{% url 'add_subtitle' %}?m={{media_object.friendly_token}}"
|
||||
style="text-decoration: none; {% if active_tab == 'subtitles' %}font-weight: bold; color: #333; padding-bottom: 3px; border-bottom: 2px solid #333;{% else %}color: #666;{% endif %}">
|
||||
{{ "Subtitles" | custom_translate:LANGUAGE_CODE}}
|
||||
</a>
|
||||
</li>
|
||||
<li style="display: inline-block;">
|
||||
<a href="{% url 'edit_video' %}?m={{media_object.friendly_token}}"
|
||||
|
||||
style="text-decoration: none; {% if active_tab == 'trim' %}font-weight: bold; color: #333; padding-bottom: 3px; border-bottom: 2px solid #333;{% else %}color: #666;{% endif %}">
|
||||
Trim
|
||||
{{ "Trim" | custom_translate:LANGUAGE_CODE}}
|
||||
</a>
|
||||
</li>
|
||||
{% comment %}
|
||||
@@ -19,7 +30,7 @@
|
||||
<a href="{% url 'edit_chapters' %}?m={{media_object.friendly_token}}"
|
||||
|
||||
style="text-decoration: none; {% if active_tab == 'chapters' %}font-weight: bold; color: #333; padding-bottom: 3px; border-bottom: 2px solid #333;{% else %}color: #666;{% endif %}">
|
||||
Chapters
|
||||
{{ "Chapters" | custom_translate:LANGUAGE_CODE}}
|
||||
</a>
|
||||
</li>
|
||||
{% endcomment %}
|
||||
@@ -27,7 +38,7 @@
|
||||
<li style="display: inline-block;">
|
||||
<a href="{% url 'publish_media' %}?m={{media_object.friendly_token}}"
|
||||
style="text-decoration: none; {% if active_tab == 'publish' %}font-weight: bold; color: #333; padding-bottom: 3px; border-bottom: 2px solid #333;{% else %}color: #666;{% endif %}">
|
||||
Publish
|
||||
{{ "Publish" | custom_translate:LANGUAGE_CODE}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
216
templates/cms/record_screen.html
Normal file
216
templates/cms/record_screen.html
Normal file
@@ -0,0 +1,216 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block headtitle %}Record Screen - {{PORTAL_NAME}}{% endblock headtitle %}
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block innercontent %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
|
||||
{% if can_add %}
|
||||
|
||||
<div class="custom-page-wrapper">
|
||||
<h2>{{ "Record Screen" | custom_translate:LANGUAGE_CODE}}</h2>
|
||||
<hr/>
|
||||
|
||||
<div style="text-align: center; padding: 40px 0;">
|
||||
<p style="margin-bottom: 20px;">{{ "Click 'Start Recording' and select the screen or tab to record. Once recording is finished, click 'Stop Recording,' and the recording will be uploaded." | custom_translate:LANGUAGE_CODE}}</p>
|
||||
<button id="startBtn" class="qq-upload-button-selector" style="padding: 10px 20px; font-size: 16px; margin-right: 10px; cursor: pointer;">{{ "Start Recording" | custom_translate:LANGUAGE_CODE}}</button>
|
||||
<button id="stopBtn" class="qq-upload-button-selector" disabled style="padding: 10px 20px; font-size: 16px; cursor: pointer;">{{ "Stop Recording" | custom_translate:LANGUAGE_CODE}}</button>
|
||||
<div id="spinner" style="display: none; margin-top: 20px;">
|
||||
<div class="spinner"></div>
|
||||
<p>video is getting uploaded</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border-left-color: #09f;
|
||||
animation: spin 1s ease infinite;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
const spinner = document.getElementById('spinner');
|
||||
|
||||
function isMobileDevice() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
if (isMobileDevice()) {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
alert('Camera recording is not supported in your browser.');
|
||||
startBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
document.querySelector('h2').textContent = 'Record Video';
|
||||
document.querySelector('p').textContent = 'Click \'Start Recording\' to start recording from your camera. Once recording is finished, click \'Stop Recording,\' and the recording will be uploaded.';
|
||||
} else {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
|
||||
alert('Screen recording is not supported in your browser. Please try a modern browser on a desktop computer.');
|
||||
startBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mediaRecorder;
|
||||
let recordedChunks = [];
|
||||
let stream;
|
||||
|
||||
startBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
if (isMobileDevice()) {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
||||
} else {
|
||||
const displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
||||
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
stream = new MediaStream([...displayStream.getTracks(), ...audioStream.getTracks()]);
|
||||
}
|
||||
|
||||
// When user stops sharing screen via browser UI
|
||||
stream.getVideoTracks()[0].addEventListener('ended', () => {
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
});
|
||||
|
||||
const mimeTypes = [
|
||||
'video/mp4',
|
||||
'video/webm;codecs=vp9',
|
||||
'video/webm;codecs=vp8',
|
||||
'video/webm',
|
||||
];
|
||||
let supportedMimeType = '';
|
||||
for (const mimeType of mimeTypes) {
|
||||
if (MediaRecorder.isTypeSupported(mimeType)) {
|
||||
supportedMimeType = mimeType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!supportedMimeType) {
|
||||
console.error("No supported mimeType found for MediaRecorder");
|
||||
alert("Your browser doesn't support any suitable video recording format.");
|
||||
return;
|
||||
}
|
||||
|
||||
mediaRecorder = new MediaRecorder(stream, { mimeType: supportedMimeType });
|
||||
|
||||
mediaRecorder.ondataavailable = event => {
|
||||
if (event.data.size > 0) {
|
||||
recordedChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstart = () => {
|
||||
stopBtn.disabled = false;
|
||||
startBtn.disabled = true;
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
stopBtn.disabled = true;
|
||||
startBtn.disabled = false;
|
||||
if(stream) {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
const blob = new Blob(recordedChunks, { type: supportedMimeType });
|
||||
recordedChunks = [];
|
||||
|
||||
uploadFile(blob);
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
} catch (err) {
|
||||
console.error("Error starting screen recording:", err);
|
||||
}
|
||||
});
|
||||
|
||||
stopBtn.addEventListener('click', () => {
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
});
|
||||
|
||||
function getCSRFToken() {
|
||||
var i, cookies, cookie, cookieVal = null;
|
||||
if ( document.cookie && '' !== document.cookie ) {
|
||||
cookies = document.cookie.split(';');
|
||||
i = 0;
|
||||
while( i < cookies.length ){
|
||||
cookie = cookies[i].trim();
|
||||
if ( 'csrftoken=' === cookie.substring(0, 10) ) {
|
||||
cookieVal = decodeURIComponent( cookie.substring(10) );
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return cookieVal;
|
||||
}
|
||||
|
||||
function uploadFile(blob) {
|
||||
const formData = new FormData();
|
||||
const extension = blob.type.includes('mp4') ? 'mp4' : 'webm';
|
||||
const recordingType = isMobileDevice() ? 'video-recording' : 'screen-recording';
|
||||
const fileName = `${recordingType}-${new Date().toISOString().slice(0, 19).replace('T', '_').replace(/:/g, '-')}.${extension}`;
|
||||
formData.append('media_file', blob, fileName);
|
||||
formData.append('title', fileName);
|
||||
|
||||
spinner.style.display = 'block';
|
||||
|
||||
fetch('/api/v1/media', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCSRFToken(),
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => { throw new Error(err.detail || 'Upload failed') });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
spinner.style.display = 'none';
|
||||
console.log('Upload successful:', data);
|
||||
if (data.friendly_token) {
|
||||
window.location.href = '/view?m=' + data.friendly_token;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
spinner.style.display = 'none';
|
||||
console.error('Upload failed:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{% else %}
|
||||
|
||||
{{can_upload_exp}}
|
||||
<br>
|
||||
<a href='/contact'>Contact</a> portal owners for more information.
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user