mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-02-25 18:55:32 -06:00
Integrate transcription in PeerTube
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import { About, ActorImageType, ActorImageType_Type, CustomConfig, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { createReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { MIMETYPES } from '@server/initializers/constants.js'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '@server/lib/local-actor.js'
|
||||
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
||||
import { ActorImageModel } from '@server/models/actor/actor-image.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { ModelCache } from '@server/models/shared/model-cache.js'
|
||||
import express from 'express'
|
||||
import { remove, writeJSON } from 'fs-extra/esm'
|
||||
import snakeCase from 'lodash-es/snakeCase.js'
|
||||
import validator from 'validator'
|
||||
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
||||
import { About, ActorImageType, ActorImageType_Type, CustomConfig, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger.js'
|
||||
import { CustomConfigAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../helpers/audit-logger.js'
|
||||
import { objectConverter } from '../../helpers/core-utils.js'
|
||||
import { CONFIG, reloadConfig } from '../../initializers/config.js'
|
||||
import { ClientHtml } from '../../lib/html/client-html.js'
|
||||
@@ -18,12 +24,6 @@ import {
|
||||
updateBannerValidator
|
||||
} from '../../middlewares/index.js'
|
||||
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config.js'
|
||||
import { createReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { MIMETYPES } from '@server/initializers/constants.js'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '@server/lib/local-actor.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { ActorImageModel } from '@server/models/actor/actor-image.js'
|
||||
import { ModelCache } from '@server/models/shared/model-cache.js'
|
||||
|
||||
const configRouter = express.Router()
|
||||
|
||||
@@ -385,6 +385,12 @@ function customConfig (): CustomConfig {
|
||||
enabled: CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED
|
||||
}
|
||||
},
|
||||
videoTranscription: {
|
||||
enabled: CONFIG.VIDEO_TRANSCRIPTION.ENABLED,
|
||||
remoteRunners: {
|
||||
enabled: CONFIG.VIDEO_TRANSCRIPTION.REMOTE_RUNNERS.ENABLED
|
||||
}
|
||||
},
|
||||
videoFile: {
|
||||
update: {
|
||||
enabled: CONFIG.VIDEO_FILE.UPDATE.ENABLED
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
import express, { UploadFiles } from 'express'
|
||||
import {
|
||||
AbortRunnerJobBody,
|
||||
AcceptRunnerJobResult,
|
||||
ErrorRunnerJobBody,
|
||||
HttpStatusCode,
|
||||
ListRunnerJobsQuery,
|
||||
LiveRTMPHLSTranscodingUpdatePayload,
|
||||
RequestRunnerJobResult,
|
||||
RunnerJobState,
|
||||
RunnerJobSuccessBody,
|
||||
RunnerJobSuccessPayload,
|
||||
RunnerJobType,
|
||||
RunnerJobUpdateBody,
|
||||
RunnerJobUpdatePayload,
|
||||
ServerErrorCode,
|
||||
TranscriptionSuccess,
|
||||
UserRight,
|
||||
VODAudioMergeTranscodingSuccess,
|
||||
VODHLSTranscodingSuccess,
|
||||
VODWebVideoTranscodingSuccess,
|
||||
VideoStudioTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { createReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
@@ -28,33 +49,13 @@ import {
|
||||
successRunnerJobValidator,
|
||||
updateRunnerJobValidator
|
||||
} from '@server/middlewares/validators/runners/index.js'
|
||||
import { RunnerModel } from '@server/models/runner/runner.js'
|
||||
import { RunnerJobModel } from '@server/models/runner/runner-job.js'
|
||||
import {
|
||||
AbortRunnerJobBody,
|
||||
AcceptRunnerJobResult,
|
||||
ErrorRunnerJobBody,
|
||||
HttpStatusCode,
|
||||
ListRunnerJobsQuery,
|
||||
LiveRTMPHLSTranscodingUpdatePayload,
|
||||
RequestRunnerJobResult,
|
||||
RunnerJobState,
|
||||
RunnerJobSuccessBody,
|
||||
RunnerJobSuccessPayload,
|
||||
RunnerJobType,
|
||||
RunnerJobUpdateBody,
|
||||
RunnerJobUpdatePayload,
|
||||
ServerErrorCode,
|
||||
UserRight,
|
||||
VideoStudioTranscodingSuccess,
|
||||
VODAudioMergeTranscodingSuccess,
|
||||
VODHLSTranscodingSuccess,
|
||||
VODWebVideoTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { RunnerModel } from '@server/models/runner/runner.js'
|
||||
import express, { UploadFiles } from 'express'
|
||||
|
||||
const postRunnerJobSuccessVideoFiles = createReqFiles(
|
||||
[ 'payload[videoFile]', 'payload[resolutionPlaylistFile]' ],
|
||||
{ ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT }
|
||||
[ 'payload[videoFile]', 'payload[resolutionPlaylistFile]', 'payload[vttFile]' ],
|
||||
{ ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT, ...MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT }
|
||||
)
|
||||
|
||||
const runnerJobUpdateVideoFiles = createReqFiles(
|
||||
@@ -345,7 +346,15 @@ const jobSuccessPayloadBuilders: {
|
||||
}
|
||||
},
|
||||
|
||||
'live-rtmp-hls-transcoding': () => ({})
|
||||
'live-rtmp-hls-transcoding': () => ({}),
|
||||
|
||||
'video-transcription': (payload: TranscriptionSuccess, files) => {
|
||||
return {
|
||||
...payload,
|
||||
|
||||
vttFile: files['payload[vttFile]'][0].path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function postRunnerJobSuccess (req: express.Request, res: express.Response) {
|
||||
|
||||
@@ -76,6 +76,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
|
||||
abuseStateChange: body.abuseStateChange,
|
||||
newPeerTubeVersion: body.newPeerTubeVersion,
|
||||
newPluginVersion: body.newPluginVersion,
|
||||
myVideoTranscriptionGenerated: body.myVideoTranscriptionGenerated,
|
||||
myVideoStudioEditionFinished: body.myVideoStudioEditionFinished
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
import express from 'express'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, VideoCaptionGenerate } from '@peertube/peertube-models'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { createLocalCaption, createTranscriptionTaskIfNeeded } from '@server/lib/video-captions.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import express from 'express'
|
||||
import { createReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||
import { MIMETYPES } from '../../../initializers/constants.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { federateVideoIfNeeded } from '../../../lib/activitypub/videos/index.js'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares/index.js'
|
||||
import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators/index.js'
|
||||
import {
|
||||
addVideoCaptionValidator,
|
||||
deleteVideoCaptionValidator,
|
||||
generateVideoCaptionValidator,
|
||||
listVideoCaptionsValidator
|
||||
} from '../../../middlewares/validators/index.js'
|
||||
import { VideoCaptionModel } from '../../../models/video/video-caption.js'
|
||||
import { createLocalCaption } from '@server/lib/video-captions.js'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'video-caption')
|
||||
|
||||
const reqVideoCaptionAdd = createReqFiles([ 'captionfile' ], MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
|
||||
|
||||
const videoCaptionsRouter = express.Router()
|
||||
|
||||
videoCaptionsRouter.post('/:videoId/captions/generate',
|
||||
authenticate,
|
||||
asyncMiddleware(generateVideoCaptionValidator),
|
||||
asyncMiddleware(createGenerateVideoCaption)
|
||||
)
|
||||
|
||||
videoCaptionsRouter.get('/:videoId/captions',
|
||||
asyncMiddleware(listVideoCaptionsValidator),
|
||||
asyncMiddleware(listVideoCaptions)
|
||||
)
|
||||
|
||||
videoCaptionsRouter.put('/:videoId/captions/:captionLanguage',
|
||||
authenticate,
|
||||
reqVideoCaptionAdd,
|
||||
asyncMiddleware(addVideoCaptionValidator),
|
||||
asyncRetryTransactionMiddleware(createVideoCaption)
|
||||
)
|
||||
|
||||
videoCaptionsRouter.delete('/:videoId/captions/:captionLanguage',
|
||||
authenticate,
|
||||
asyncMiddleware(deleteVideoCaptionValidator),
|
||||
@@ -40,6 +56,19 @@ export {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function createGenerateVideoCaption (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
|
||||
const body = req.body as VideoCaptionGenerate
|
||||
if (body.forceTranscription === true) {
|
||||
await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingTranscription')
|
||||
}
|
||||
|
||||
await createTranscriptionTaskIfNeeded(video)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function listVideoCaptions (req: express.Request, res: express.Response) {
|
||||
const data = await VideoCaptionModel.listVideoCaptions(res.locals.onlyVideo.id)
|
||||
|
||||
@@ -74,7 +103,7 @@ async function deleteVideoCaption (req: express.Request, res: express.Response)
|
||||
await federateVideoIfNeeded(video, false, t)
|
||||
})
|
||||
|
||||
logger.info('Video caption %s of video %s deleted.', videoCaption.language, video.uuid)
|
||||
logger.info('Video caption %s of video %s deleted.', videoCaption.language, video.uuid, lTags(video.uuid))
|
||||
|
||||
Hooks.runAction('action:api.video-caption.deleted', { caption: videoCaption, req, res })
|
||||
|
||||
|
||||
@@ -142,7 +142,8 @@ async function handleTorrentImport (req: express.Request, res: express.Response,
|
||||
? 'torrent-file'
|
||||
: 'magnet-uri',
|
||||
videoImportId: videoImport.id,
|
||||
preventException: false
|
||||
preventException: false,
|
||||
generateTranscription: body.generateTranscription
|
||||
}
|
||||
await JobQueue.Instance.createJob({ type: 'video-import', payload })
|
||||
|
||||
|
||||
@@ -1,83 +1,74 @@
|
||||
import { UploadFilesForCheck } from 'express'
|
||||
import validator from 'validator'
|
||||
import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants.js'
|
||||
import {
|
||||
LiveRTMPHLSTranscodingSuccess,
|
||||
RunnerJobSuccessPayload,
|
||||
RunnerJobType,
|
||||
RunnerJobUpdatePayload,
|
||||
VideoStudioTranscodingSuccess,
|
||||
TranscriptionSuccess,
|
||||
VODAudioMergeTranscodingSuccess,
|
||||
VODHLSTranscodingSuccess,
|
||||
VODWebVideoTranscodingSuccess
|
||||
VODWebVideoTranscodingSuccess,
|
||||
VideoStudioTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants.js'
|
||||
import { UploadFilesForCheck } from 'express'
|
||||
import validator from 'validator'
|
||||
import { exists, isArray, isFileValid, isSafeFilename } from '../misc.js'
|
||||
|
||||
const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS
|
||||
|
||||
const runnerJobTypes = new Set([ 'vod-hls-transcoding', 'vod-web-video-transcoding', 'vod-audio-merge-transcoding' ])
|
||||
function isRunnerJobTypeValid (value: RunnerJobType) {
|
||||
export function isRunnerJobTypeValid (value: RunnerJobType) {
|
||||
return runnerJobTypes.has(value)
|
||||
}
|
||||
|
||||
function isRunnerJobSuccessPayloadValid (value: RunnerJobSuccessPayload, type: RunnerJobType, files: UploadFilesForCheck) {
|
||||
export function isRunnerJobSuccessPayloadValid (value: RunnerJobSuccessPayload, type: RunnerJobType, files: UploadFilesForCheck) {
|
||||
return isRunnerJobVODWebVideoResultPayloadValid(value as VODWebVideoTranscodingSuccess, type, files) ||
|
||||
isRunnerJobVODHLSResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
|
||||
isRunnerJobVODAudioMergeResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
|
||||
isRunnerJobLiveRTMPHLSResultPayloadValid(value as LiveRTMPHLSTranscodingSuccess, type) ||
|
||||
isRunnerJobVideoStudioResultPayloadValid(value as VideoStudioTranscodingSuccess, type, files)
|
||||
isRunnerJobVideoStudioResultPayloadValid(value as VideoStudioTranscodingSuccess, type, files) ||
|
||||
isRunnerJobTranscriptionResultPayloadValid(value as TranscriptionSuccess, type, files)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRunnerJobProgressValid (value: string) {
|
||||
export function isRunnerJobProgressValid (value: string) {
|
||||
return validator.default.isInt(value + '', RUNNER_JOBS_CONSTRAINTS_FIELDS.PROGRESS)
|
||||
}
|
||||
|
||||
function isRunnerJobUpdatePayloadValid (value: RunnerJobUpdatePayload, type: RunnerJobType, files: UploadFilesForCheck) {
|
||||
export function isRunnerJobUpdatePayloadValid (value: RunnerJobUpdatePayload, type: RunnerJobType, files: UploadFilesForCheck) {
|
||||
return isRunnerJobVODWebVideoUpdatePayloadValid(value, type, files) ||
|
||||
isRunnerJobVODHLSUpdatePayloadValid(value, type, files) ||
|
||||
isRunnerJobVideoStudioUpdatePayloadValid(value, type, files) ||
|
||||
isRunnerJobVODAudioMergeUpdatePayloadValid(value, type, files) ||
|
||||
isRunnerJobLiveRTMPHLSUpdatePayloadValid(value, type, files)
|
||||
isRunnerJobLiveRTMPHLSUpdatePayloadValid(value, type, files) ||
|
||||
isRunnerJobTranscriptionUpdatePayloadValid(value, type, files)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRunnerJobTokenValid (value: string) {
|
||||
export function isRunnerJobTokenValid (value: string) {
|
||||
return exists(value) && validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.TOKEN)
|
||||
}
|
||||
|
||||
function isRunnerJobAbortReasonValid (value: string) {
|
||||
export function isRunnerJobAbortReasonValid (value: string) {
|
||||
return validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.REASON)
|
||||
}
|
||||
|
||||
function isRunnerJobErrorMessageValid (value: string) {
|
||||
export function isRunnerJobErrorMessageValid (value: string) {
|
||||
return validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE)
|
||||
}
|
||||
|
||||
function isRunnerJobStateValid (value: any) {
|
||||
export function isRunnerJobStateValid (value: any) {
|
||||
return exists(value) && RUNNER_JOB_STATES[value] !== undefined
|
||||
}
|
||||
|
||||
function isRunnerJobArrayOfStateValid (value: any) {
|
||||
export function isRunnerJobArrayOfStateValid (value: any) {
|
||||
return isArray(value) && value.every(v => isRunnerJobStateValid(v))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isRunnerJobTypeValid,
|
||||
isRunnerJobSuccessPayloadValid,
|
||||
isRunnerJobUpdatePayloadValid,
|
||||
isRunnerJobTokenValid,
|
||||
isRunnerJobErrorMessageValid,
|
||||
isRunnerJobProgressValid,
|
||||
isRunnerJobAbortReasonValid,
|
||||
isRunnerJobArrayOfStateValid,
|
||||
isRunnerJobStateValid
|
||||
}
|
||||
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRunnerJobVODWebVideoResultPayloadValid (
|
||||
@@ -124,6 +115,15 @@ function isRunnerJobVideoStudioResultPayloadValid (
|
||||
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
|
||||
}
|
||||
|
||||
function isRunnerJobTranscriptionResultPayloadValid (
|
||||
value: TranscriptionSuccess,
|
||||
type: RunnerJobType,
|
||||
files: UploadFilesForCheck
|
||||
) {
|
||||
return type === 'video-transcription' &&
|
||||
isFileValid({ files, field: 'payload[vttFile]', mimeTypeRegex: null, maxSize: null })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRunnerJobVODWebVideoUpdatePayloadValid (
|
||||
@@ -153,6 +153,15 @@ function isRunnerJobVODAudioMergeUpdatePayloadValid (
|
||||
(!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
||||
}
|
||||
|
||||
function isRunnerJobTranscriptionUpdatePayloadValid (
|
||||
value: RunnerJobUpdatePayload,
|
||||
type: RunnerJobType,
|
||||
_files: UploadFilesForCheck
|
||||
) {
|
||||
return type === 'video-transcription' &&
|
||||
(!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
||||
}
|
||||
|
||||
function isRunnerJobLiveRTMPHLSUpdatePayloadValid (
|
||||
value: RunnerJobUpdatePayload,
|
||||
type: RunnerJobType,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { decacheModule } from '@server/helpers/decache.js'
|
||||
import { buildPath, root } from '@peertube/peertube-node-utils'
|
||||
import { parseBytes, parseDurationToMs } from '../helpers/core-utils.js'
|
||||
import { TranscriptionEngineName, WhisperBuiltinModelName } from '@peertube/peertube-transcription'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
let config: IConfig = require('config')
|
||||
@@ -515,6 +516,16 @@ const CONFIG = {
|
||||
get ENABLED () { return config.get<boolean>('video_file.update.enabled') }
|
||||
}
|
||||
},
|
||||
VIDEO_TRANSCRIPTION: {
|
||||
get ENABLED () { return config.get<boolean>('video_transcription.enabled') },
|
||||
get ENGINE () { return config.get<TranscriptionEngineName>('video_transcription.engine') },
|
||||
get ENGINE_PATH () { return config.get<string>('video_transcription.engine_path') },
|
||||
get MODEL () { return config.get<WhisperBuiltinModelName>('video_transcription.model') },
|
||||
get MODEL_PATH () { return config.get<string>('video_transcription.model_path') },
|
||||
REMOTE_RUNNERS: {
|
||||
get ENABLED () { return config.get<boolean>('video_transcription.remote_runners.enabled') }
|
||||
}
|
||||
},
|
||||
IMPORT: {
|
||||
VIDEOS: {
|
||||
get CONCURRENCY () { return config.get<number>('import.videos.concurrency') },
|
||||
|
||||
@@ -47,7 +47,7 @@ import { cpus } from 'os'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 850
|
||||
const LAST_MIGRATION_VERSION = 855
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -213,7 +213,8 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = {
|
||||
'notify': 1,
|
||||
'federate-video': 1,
|
||||
'create-user-export': 1,
|
||||
'import-user-archive': 1
|
||||
'import-user-archive': 1,
|
||||
'video-transcription': 1
|
||||
}
|
||||
// Excluded keys are jobs that can be configured by admins
|
||||
const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = {
|
||||
@@ -241,7 +242,8 @@ const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-im
|
||||
'notify': 5,
|
||||
'federate-video': 3,
|
||||
'create-user-export': 1,
|
||||
'import-user-archive': 1
|
||||
'import-user-archive': 1,
|
||||
'video-transcription': 1
|
||||
}
|
||||
const JOB_TTL: { [id in JobType]: number } = {
|
||||
'activitypub-http-broadcast': 60000 * 10, // 10 minutes
|
||||
@@ -270,7 +272,8 @@ const JOB_TTL: { [id in JobType]: number } = {
|
||||
'notify': 60000 * 5, // 5 minutes
|
||||
'federate-video': 60000 * 5, // 5 minutes,
|
||||
'create-user-export': 60000 * 60 * 24, // 24 hours
|
||||
'import-user-archive': 60000 * 60 * 24 // 24 hours
|
||||
'import-user-archive': 60000 * 60 * 24, // 24 hours
|
||||
'video-transcription': 1000 * 3600 * 6 // 6 hours
|
||||
}
|
||||
const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
||||
'videos-views-stats': {
|
||||
@@ -282,7 +285,8 @@ const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
||||
}
|
||||
const JOB_PRIORITY = {
|
||||
TRANSCODING: 100,
|
||||
VIDEO_STUDIO: 150
|
||||
VIDEO_STUDIO: 150,
|
||||
TRANSCRIPTION: 200
|
||||
}
|
||||
|
||||
const JOB_REMOVAL_OPTIONS = {
|
||||
@@ -1013,7 +1017,9 @@ const DIRECTORIES = {
|
||||
|
||||
ORIGINAL_VIDEOS: CONFIG.STORAGE.ORIGINAL_VIDEO_FILES_DIR,
|
||||
|
||||
HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls')
|
||||
HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls'),
|
||||
|
||||
LOCAL_PIP_DIRECTORY: join(CONFIG.STORAGE.BIN_DIR, 'pip')
|
||||
}
|
||||
|
||||
const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING_RESUMABLE_UPLOADS
|
||||
|
||||
67
server/core/initializers/migrations/0855-transcription.ts
Normal file
67
server/core/initializers/migrations/0855-transcription.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
}): Promise<void> {
|
||||
const { transaction } = utils
|
||||
|
||||
// Notification
|
||||
{
|
||||
await utils.queryInterface.addColumn('userNotification', 'videoCaptionId', {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'videoCaption',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
}, { transaction })
|
||||
}
|
||||
|
||||
// Notification settings
|
||||
{
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true
|
||||
}
|
||||
await utils.queryInterface.addColumn('userNotificationSetting', 'myVideoTranscriptionGenerated', data, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const query = 'UPDATE "userNotificationSetting" SET "myVideoTranscriptionGenerated" = 1'
|
||||
await utils.sequelize.query(query, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: false
|
||||
}
|
||||
await utils.queryInterface.changeColumn('userNotificationSetting', 'myVideoTranscriptionGenerated', data, { transaction })
|
||||
}
|
||||
}
|
||||
|
||||
// Video job info
|
||||
{
|
||||
await utils.queryInterface.addColumn('videoJobInfo', 'pendingTranscription', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
}, { transaction })
|
||||
}
|
||||
}
|
||||
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
down, up
|
||||
}
|
||||
@@ -24,11 +24,13 @@ import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
||||
import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job.js'
|
||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||
import { createTranscriptionTaskIfNeeded } from '@server/lib/video-captions.js'
|
||||
import { replaceChaptersIfNotExist } from '@server/lib/video-chapters.js'
|
||||
import { buildNewFile } from '@server/lib/video-file.js'
|
||||
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { buildNextVideoState } from '@server/lib/video-state.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import { MVideoImport, MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import.js'
|
||||
import { Job } from 'bullmq'
|
||||
@@ -84,7 +86,7 @@ export {
|
||||
async function processTorrentImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportTorrentPayload) {
|
||||
logger.info('Processing torrent video import in job %s.', job.id)
|
||||
|
||||
const options = { type: payload.type, videoImportId: payload.videoImportId }
|
||||
const options = { type: payload.type, generateTranscription: payload.generateTranscription, videoImportId: payload.videoImportId }
|
||||
|
||||
const target = {
|
||||
torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined,
|
||||
@@ -96,7 +98,7 @@ async function processTorrentImport (job: Job, videoImport: MVideoImportDefault,
|
||||
async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportYoutubeDLPayload) {
|
||||
logger.info('Processing youtubeDL video import in job %s.', job.id)
|
||||
|
||||
const options = { type: payload.type, videoImportId: videoImport.id }
|
||||
const options = { type: payload.type, generateTranscription: payload.generateTranscription, videoImportId: videoImport.id }
|
||||
|
||||
const youtubeDL = new YoutubeDLWrapper(
|
||||
videoImport.targetUrl,
|
||||
@@ -122,6 +124,7 @@ async function getVideoImportOrDie (payload: VideoImportPayload) {
|
||||
|
||||
type ProcessFileOptions = {
|
||||
type: VideoImportYoutubeDLPayloadType | VideoImportTorrentPayloadType
|
||||
generateTranscription: boolean
|
||||
videoImportId: number
|
||||
}
|
||||
async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
|
||||
@@ -228,7 +231,14 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
||||
})
|
||||
})
|
||||
|
||||
await afterImportSuccess({ videoImport: videoImportUpdated, video, videoFile, user: videoImport.User, videoFileAlreadyLocked: true })
|
||||
await afterImportSuccess({
|
||||
videoImport: videoImportUpdated,
|
||||
video,
|
||||
videoFile,
|
||||
user: videoImport.User,
|
||||
generateTranscription: options.generateTranscription,
|
||||
videoFileAlreadyLocked: true
|
||||
})
|
||||
} finally {
|
||||
videoFileLockReleaser()
|
||||
}
|
||||
@@ -277,9 +287,12 @@ async function afterImportSuccess (options: {
|
||||
video: MVideoFullLight
|
||||
videoFile: MVideoFile
|
||||
user: MUserId
|
||||
|
||||
videoFileAlreadyLocked: boolean
|
||||
|
||||
generateTranscription: boolean
|
||||
}) {
|
||||
const { video, videoFile, videoImport, user, videoFileAlreadyLocked } = options
|
||||
const { video, videoFile, videoImport, user, generateTranscription, videoFileAlreadyLocked } = options
|
||||
|
||||
Notifier.Instance.notifyOnFinishedVideoImport({ videoImport: Object.assign(videoImport, { Video: video }), success: true })
|
||||
|
||||
@@ -294,6 +307,10 @@ async function afterImportSuccess (options: {
|
||||
// Generate the storyboard in the job queue, and don't forget to federate an update after
|
||||
await JobQueue.Instance.createJob(buildStoryboardJobIfNeeded({ video, federate: true }))
|
||||
|
||||
if (await VideoCaptionModel.hasVideoCaption(video.id) !== true && generateTranscription === true) {
|
||||
await createTranscriptionTaskIfNeeded(video)
|
||||
}
|
||||
|
||||
if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
|
||||
await JobQueue.Instance.createJob(
|
||||
await buildMoveJob({ video, previousVideoState: VideoState.TO_IMPORT, type: 'move-to-object-storage' })
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Job } from 'bullmq'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
|
||||
import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@peertube/peertube-models'
|
||||
import { peertubeTruncate } from '@server/helpers/core-utils.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url.js'
|
||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
|
||||
@@ -16,7 +14,10 @@ import {
|
||||
} from '@server/lib/paths.js'
|
||||
import { generateLocalVideoMiniature, regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js'
|
||||
import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding.js'
|
||||
import { createTranscriptionTaskIfNeeded } from '@server/lib/video-captions.js'
|
||||
import { buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { isVideoInPublicDirectory } from '@server/lib/video-privacy.js'
|
||||
import { moveToNextState } from '@server/lib/video-state.js'
|
||||
import { VideoBlacklistModel } from '@server/models/video/video-blacklist.js'
|
||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
@@ -26,11 +27,12 @@ import { VideoLiveModel } from '@server/models/video/video-live.js'
|
||||
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@server/types/models/index.js'
|
||||
import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
|
||||
import { Job } from 'bullmq'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||
import { JobQueue } from '../job-queue.js'
|
||||
import { isVideoInPublicDirectory } from '@server/lib/video-privacy.js'
|
||||
import { buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
||||
|
||||
const lTags = loggerTagsFactory('live', 'job')
|
||||
|
||||
@@ -174,9 +176,13 @@ async function saveReplayToExternalVideo (options: {
|
||||
await replayVideo.addAndSaveThumbnail(thumbnail)
|
||||
}
|
||||
|
||||
await moveToNextState({ video: replayVideo, isNewVideo: true })
|
||||
|
||||
await createStoryboardJob(replayVideo)
|
||||
|
||||
if (CONFIG.VIDEO_TRANSCRIPTION.ENABLED === true) {
|
||||
await createTranscriptionTaskIfNeeded(replayVideo)
|
||||
}
|
||||
|
||||
await moveToNextState({ video: replayVideo, isNewVideo: true })
|
||||
}
|
||||
|
||||
async function replaceLiveByReplay (options: {
|
||||
@@ -245,6 +251,10 @@ async function replaceLiveByReplay (options: {
|
||||
await moveToNextState({ video: videoWithFiles, isNewVideo: true })
|
||||
|
||||
await createStoryboardJob(videoWithFiles)
|
||||
|
||||
if (CONFIG.VIDEO_TRANSCRIPTION.ENABLED === true) {
|
||||
await createTranscriptionTaskIfNeeded(videoWithFiles)
|
||||
}
|
||||
}
|
||||
|
||||
async function assignReplayFilesToVideo (options: {
|
||||
|
||||
21
server/core/lib/job-queue/handlers/video-transcription.ts
Normal file
21
server/core/lib/job-queue/handlers/video-transcription.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { VideoTranscriptionPayload } from '@peertube/peertube-models'
|
||||
import { generateSubtitle } from '@server/lib/video-captions.js'
|
||||
import { Job } from 'bullmq'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
|
||||
const lTags = loggerTagsFactory('transcription')
|
||||
|
||||
export async function processVideoTranscription (job: Job) {
|
||||
const payload = job.data as VideoTranscriptionPayload
|
||||
|
||||
logger.info('Processing video transcription in job %s.', job.id)
|
||||
|
||||
const video = await VideoModel.load(payload.videoUUID)
|
||||
if (!video) {
|
||||
logger.info('Do not process transcription job %d, video does not exist.', job.id, lTags(payload.videoUUID))
|
||||
return
|
||||
}
|
||||
|
||||
return generateSubtitle({ video })
|
||||
}
|
||||
@@ -1,15 +1,3 @@
|
||||
import {
|
||||
FlowJob,
|
||||
FlowProducer,
|
||||
Job,
|
||||
JobsOptions,
|
||||
Queue,
|
||||
QueueEvents,
|
||||
QueueEventsOptions,
|
||||
QueueOptions,
|
||||
Worker,
|
||||
WorkerOptions
|
||||
} from 'bullmq'
|
||||
import { pick, timeoutPromise } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
ActivitypubFollowPayload,
|
||||
@@ -37,12 +25,25 @@ import {
|
||||
VideoLiveEndingPayload,
|
||||
VideoRedundancyPayload,
|
||||
VideoStudioEditionPayload,
|
||||
VideoTranscodingPayload
|
||||
VideoTranscodingPayload,
|
||||
VideoTranscriptionPayload
|
||||
} from '@peertube/peertube-models'
|
||||
import { parseDurationToMs } from '@server/helpers/core-utils.js'
|
||||
import { jobStates } from '@server/helpers/custom-validators/jobs.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy.js'
|
||||
import {
|
||||
FlowJob,
|
||||
FlowProducer,
|
||||
Job,
|
||||
JobsOptions,
|
||||
Queue,
|
||||
QueueEvents,
|
||||
QueueEventsOptions,
|
||||
QueueOptions,
|
||||
Worker,
|
||||
WorkerOptions
|
||||
} from 'bullmq'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
import { JOB_ATTEMPTS, JOB_CONCURRENCY, JOB_REMOVAL_OPTIONS, JOB_TTL, REPEAT_JOBS, WEBSERVER } from '../../initializers/constants.js'
|
||||
import { Hooks } from '../plugins/hooks.js'
|
||||
@@ -58,10 +59,13 @@ import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unica
|
||||
import { refreshAPObject } from './handlers/activitypub-refresher.js'
|
||||
import { processActorKeys } from './handlers/actor-keys.js'
|
||||
import { processAfterVideoChannelImport } from './handlers/after-video-channel-import.js'
|
||||
import { processCreateUserExport } from './handlers/create-user-export.js'
|
||||
import { processEmail } from './handlers/email.js'
|
||||
import { processFederateVideo } from './handlers/federate-video.js'
|
||||
import { processGenerateStoryboard } from './handlers/generate-storyboard.js'
|
||||
import { processImportUserArchive } from './handlers/import-user-archive.js'
|
||||
import { processManageVideoTorrent } from './handlers/manage-video-torrent.js'
|
||||
import { onMoveToFileSystemFailure, processMoveToFileSystem } from './handlers/move-to-file-system.js'
|
||||
import { onMoveToObjectStorageFailure, processMoveToObjectStorage } from './handlers/move-to-object-storage.js'
|
||||
import { processNotify } from './handlers/notify.js'
|
||||
import { processTranscodingJobBuilder } from './handlers/transcoding-job-builder.js'
|
||||
@@ -71,10 +75,8 @@ import { processVideoImport } from './handlers/video-import.js'
|
||||
import { processVideoLiveEnding } from './handlers/video-live-ending.js'
|
||||
import { processVideoStudioEdition } from './handlers/video-studio-edition.js'
|
||||
import { processVideoTranscoding } from './handlers/video-transcoding.js'
|
||||
import { processVideoTranscription } from './handlers/video-transcription.js'
|
||||
import { processVideosViewsStats } from './handlers/video-views-stats.js'
|
||||
import { onMoveToFileSystemFailure, processMoveToFileSystem } from './handlers/move-to-file-system.js'
|
||||
import { processCreateUserExport } from './handlers/create-user-export.js'
|
||||
import { processImportUserArchive } from './handlers/import-user-archive.js'
|
||||
|
||||
export type CreateJobArgument =
|
||||
{ type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
|
||||
@@ -104,7 +106,8 @@ export type CreateJobArgument =
|
||||
{ type: 'federate-video', payload: FederateVideoPayload } |
|
||||
{ type: 'create-user-export', payload: CreateUserExportPayload } |
|
||||
{ type: 'generate-video-storyboard', payload: GenerateStoryboardPayload } |
|
||||
{ type: 'import-user-archive', payload: ImportUserArchivePayload }
|
||||
{ type: 'import-user-archive', payload: ImportUserArchivePayload } |
|
||||
{ type: 'video-transcription', payload: VideoTranscriptionPayload }
|
||||
|
||||
export type CreateJobOptions = {
|
||||
delay?: number
|
||||
@@ -139,7 +142,8 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = {
|
||||
'videos-views-stats': processVideosViewsStats,
|
||||
'generate-video-storyboard': processGenerateStoryboard,
|
||||
'create-user-export': processCreateUserExport,
|
||||
'import-user-archive': processImportUserArchive
|
||||
'import-user-archive': processImportUserArchive,
|
||||
'video-transcription': processVideoTranscription
|
||||
}
|
||||
|
||||
const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = {
|
||||
@@ -171,10 +175,11 @@ const jobTypes: JobType[] = [
|
||||
'video-live-ending',
|
||||
'video-redundancy',
|
||||
'video-studio-edition',
|
||||
'video-transcoding',
|
||||
'video-transcription',
|
||||
'videos-views-stats',
|
||||
'create-user-export',
|
||||
'import-user-archive'
|
||||
'import-user-archive',
|
||||
'video-transcoding'
|
||||
]
|
||||
|
||||
const silentFailure = new Set<JobType>([ 'activitypub-http-unicast' ])
|
||||
@@ -561,6 +566,5 @@ class JobQueue {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
jobTypes,
|
||||
JobQueue
|
||||
JobQueue, jobTypes
|
||||
}
|
||||
|
||||
@@ -107,6 +107,8 @@ export class LocalVideoCreator {
|
||||
this.channel = options.channel
|
||||
|
||||
this.videoAttributeResultHook = options.videoAttributeResultHook
|
||||
|
||||
this.lTags = options.lTags
|
||||
}
|
||||
|
||||
async create () {
|
||||
@@ -201,8 +203,11 @@ export class LocalVideoCreator {
|
||||
|
||||
if (this.videoFile) {
|
||||
transaction.afterCommit(() => {
|
||||
addVideoJobsAfterCreation({ video: this.video, videoFile: this.videoFile })
|
||||
.catch(err => logger.error('Cannot build new video jobs of %s.', this.video.uuid, { err, ...this.lTags(this.video.uuid) }))
|
||||
addVideoJobsAfterCreation({
|
||||
video: this.video,
|
||||
videoFile: this.videoFile,
|
||||
generateTranscription: this.videoAttributes.generateTranscription ?? true
|
||||
}).catch(err => logger.error('Cannot build new video jobs of %s.', this.video.uuid, { err, ...this.lTags(this.video.uuid) }))
|
||||
})
|
||||
} else {
|
||||
await federateVideoIfNeeded(this.video, true, transaction)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MCommentOwnerVideo,
|
||||
MPlugin,
|
||||
MVideoAccountLight,
|
||||
MVideoCaptionVideo,
|
||||
MVideoFullLight
|
||||
} from '../../types/models/index.js'
|
||||
import { JobQueue } from '../job-queue/index.js'
|
||||
@@ -41,7 +42,8 @@ import {
|
||||
OwnedPublicationAfterTranscoding,
|
||||
RegistrationRequestForModerators,
|
||||
StudioEditionFinishedForOwner,
|
||||
UnblacklistForOwner
|
||||
UnblacklistForOwner,
|
||||
VideoTranscriptionGeneratedForOwner
|
||||
} from './shared/index.js'
|
||||
|
||||
const lTags = loggerTagsFactory('notifier')
|
||||
@@ -69,7 +71,8 @@ class Notifier {
|
||||
newAbuseMessage: [ NewAbuseMessageForReporter, NewAbuseMessageForModerators ],
|
||||
newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
|
||||
newPluginVersion: [ NewPluginVersionForAdmins ],
|
||||
videoStudioEditionFinished: [ StudioEditionFinishedForOwner ]
|
||||
videoStudioEditionFinished: [ StudioEditionFinishedForOwner ],
|
||||
videoTranscriptionGenerated: [ VideoTranscriptionGeneratedForOwner ]
|
||||
}
|
||||
|
||||
private static instance: Notifier
|
||||
@@ -275,6 +278,16 @@ class Notifier {
|
||||
.catch(err => logger.error('Cannot notify on finished studio edition %s.', video.url, { err }))
|
||||
}
|
||||
|
||||
notifyOfGeneratedVideoTranscription (caption: MVideoCaptionVideo) {
|
||||
const models = this.notificationModels.videoTranscriptionGenerated
|
||||
const video = caption.Video
|
||||
|
||||
logger.debug('Notify on generated video transcription', { language: caption.language, video: video.url, ...lTags() })
|
||||
|
||||
this.sendNotifications(models, caption)
|
||||
.catch(err => logger.error('Cannot notify on generated video transcription %s of video %s.', caption.language, video.url, { err }))
|
||||
}
|
||||
|
||||
private async notify <T> (object: AbstractNotification<T>) {
|
||||
await object.prepare()
|
||||
|
||||
|
||||
1
server/core/lib/notifier/shared/caption/index.ts
Normal file
1
server/core/lib/notifier/shared/caption/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './video-transcription-generated-for-owner.js'
|
||||
@@ -0,0 +1,63 @@
|
||||
import { UserNotificationType } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VIDEO_LANGUAGES, WEBSERVER } from '@server/initializers/constants.js'
|
||||
import { UserNotificationModel } from '@server/models/user/user-notification.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { MUserDefault, MUserWithNotificationSetting, MVideoCaptionVideo, UserNotificationModelForApi } from '@server/types/models/index.js'
|
||||
import { AbstractNotification } from '../common/abstract-notification.js'
|
||||
|
||||
export class VideoTranscriptionGeneratedForOwner extends AbstractNotification <MVideoCaptionVideo> {
|
||||
private user: MUserDefault
|
||||
|
||||
async prepare () {
|
||||
this.user = await UserModel.loadByVideoId(this.payload.videoId)
|
||||
}
|
||||
|
||||
log () {
|
||||
logger.info(
|
||||
'Notifying user %s the transcription %s of video %s is generated.',
|
||||
this.user.username, this.payload.language, this.payload.Video.url
|
||||
)
|
||||
}
|
||||
|
||||
getSetting (user: MUserWithNotificationSetting) {
|
||||
return user.NotificationSetting.myVideoTranscriptionGenerated
|
||||
}
|
||||
|
||||
getTargetUsers () {
|
||||
if (!this.user) return []
|
||||
|
||||
return [ this.user ]
|
||||
}
|
||||
|
||||
createNotification (user: MUserWithNotificationSetting) {
|
||||
const notification = UserNotificationModel.build<UserNotificationModelForApi>({
|
||||
type: UserNotificationType.MY_VIDEO_TRANSCRIPTION_GENERATED,
|
||||
userId: user.id,
|
||||
videoCaptionId: this.payload.id
|
||||
})
|
||||
notification.VideoCaption = this.payload
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
createEmail (to: string) {
|
||||
const video = this.payload.Video
|
||||
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
|
||||
|
||||
const language = VIDEO_LANGUAGES[this.payload.language]
|
||||
|
||||
return {
|
||||
to,
|
||||
subject: `Transcription in ${language} of your video ${video.name} has been generated`,
|
||||
text: `Transcription in ${language} of your video ${video.name} has been generated.`,
|
||||
locals: {
|
||||
title: 'Transcription has been generated',
|
||||
action: {
|
||||
text: 'View video',
|
||||
url: videoUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './abuse/index.js'
|
||||
export * from './blacklist/index.js'
|
||||
export * from './caption/index.js'
|
||||
export * from './comment/index.js'
|
||||
export * from './common/index.js'
|
||||
export * from './follow/index.js'
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
RunnerJobStateType,
|
||||
RunnerJobStudioTranscodingPayload,
|
||||
RunnerJobSuccessPayload,
|
||||
RunnerJobTranscriptionPayload,
|
||||
RunnerJobTranscriptionPrivatePayload,
|
||||
RunnerJobType,
|
||||
RunnerJobUpdatePayload,
|
||||
RunnerJobVODAudioMergeTranscodingPayload,
|
||||
@@ -51,6 +53,11 @@ type CreateRunnerJobArg =
|
||||
type: Extract<RunnerJobType, 'video-studio-transcoding'>
|
||||
payload: RunnerJobStudioTranscodingPayload
|
||||
privatePayload: RunnerJobVideoStudioTranscodingPrivatePayload
|
||||
} |
|
||||
{
|
||||
type: Extract<RunnerJobType, 'video-transcription'>
|
||||
payload: RunnerJobTranscriptionPayload
|
||||
privatePayload: RunnerJobTranscriptionPrivatePayload
|
||||
}
|
||||
|
||||
export abstract class AbstractJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { AbstractJobHandler } from './abstract-job-handler.js'
|
||||
import { loadTranscodingRunnerVideo } from './shared/index.js'
|
||||
import { loadRunnerVideo } from './shared/utils.js'
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export abstract class AbstractVODTranscodingJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> extends AbstractJobHandler<C, U, S> {
|
||||
@@ -38,7 +38,7 @@ export abstract class AbstractVODTranscodingJobHandler <C, U extends RunnerJobUp
|
||||
}) {
|
||||
if (options.nextState !== RunnerJobState.ERRORED) return
|
||||
|
||||
const video = await loadTranscodingRunnerVideo(options.runnerJob, this.lTags)
|
||||
const video = await loadRunnerVideo(options.runnerJob, this.lTags)
|
||||
if (!video) return
|
||||
|
||||
await moveToFailedTranscodingState(video)
|
||||
@@ -51,7 +51,7 @@ export abstract class AbstractVODTranscodingJobHandler <C, U extends RunnerJobUp
|
||||
}) {
|
||||
const { runnerJob } = options
|
||||
|
||||
const video = await loadTranscodingRunnerVideo(options.runnerJob, this.lTags)
|
||||
const video = await loadRunnerVideo(options.runnerJob, this.lTags)
|
||||
if (!video) return
|
||||
|
||||
const pending = await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './abstract-job-handler.js'
|
||||
export * from './live-rtmp-hls-transcoding-job-handler.js'
|
||||
export * from './runner-job-handlers.js'
|
||||
export * from './transcription-job-handler.js'
|
||||
export * from './video-studio-transcoding-job-handler.js'
|
||||
export * from './vod-audio-merge-transcoding-job-handler.js'
|
||||
export * from './vod-hls-transcoding-job-handler.js'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { RunnerJobSuccessPayload, RunnerJobType, RunnerJobUpdatePayload } from '@peertube/peertube-models'
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { AbstractJobHandler } from './abstract-job-handler.js'
|
||||
import { LiveRTMPHLSTranscodingJobHandler } from './live-rtmp-hls-transcoding-job-handler.js'
|
||||
import { TranscriptionJobHandler } from './transcription-job-handler.js'
|
||||
import { VideoStudioTranscodingJobHandler } from './video-studio-transcoding-job-handler.js'
|
||||
import { VODAudioMergeTranscodingJobHandler } from './vod-audio-merge-transcoding-job-handler.js'
|
||||
import { VODHLSTranscodingJobHandler } from './vod-hls-transcoding-job-handler.js'
|
||||
@@ -12,7 +13,8 @@ const processors: Record<RunnerJobType, new() => AbstractJobHandler<unknown, Run
|
||||
'vod-hls-transcoding': VODHLSTranscodingJobHandler,
|
||||
'vod-audio-merge-transcoding': VODAudioMergeTranscodingJobHandler,
|
||||
'live-rtmp-hls-transcoding': LiveRTMPHLSTranscodingJobHandler,
|
||||
'video-studio-transcoding': VideoStudioTranscodingJobHandler
|
||||
'video-studio-transcoding': VideoStudioTranscodingJobHandler,
|
||||
'video-transcription': TranscriptionJobHandler
|
||||
}
|
||||
|
||||
export function getRunnerJobHandlerClass (job: MRunnerJob) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './vod-helpers.js'
|
||||
@@ -23,12 +23,12 @@ export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
|
||||
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
|
||||
}
|
||||
|
||||
export async function loadTranscodingRunnerVideo (runnerJob: MRunnerJob, lTags: LoggerTagsFn) {
|
||||
export async function loadRunnerVideo (runnerJob: MRunnerJob, lTags: LoggerTagsFn) {
|
||||
const videoUUID = runnerJob.privatePayload.videoUUID
|
||||
|
||||
const video = await VideoModel.loadFull(videoUUID)
|
||||
if (!video) {
|
||||
logger.info('Video %s does not exist anymore after transcoding runner job.', videoUUID, lTags(videoUUID))
|
||||
logger.info('Video %s does not exist anymore after runner job.', videoUUID, lTags(videoUUID))
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
RunnerJobState,
|
||||
RunnerJobStateType,
|
||||
RunnerJobTranscriptionPayload,
|
||||
RunnerJobTranscriptionPrivatePayload,
|
||||
RunnerJobUpdatePayload,
|
||||
TranscriptionSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { JOB_PRIORITY } from '@server/initializers/constants.js'
|
||||
import { onTranscriptionEnded } from '@server/lib/video-captions.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MVideoUUID } from '@server/types/models/index.js'
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls.js'
|
||||
import { AbstractJobHandler } from './abstract-job-handler.js'
|
||||
import { loadRunnerVideo } from './shared/utils.js'
|
||||
|
||||
type CreateOptions = {
|
||||
video: MVideoUUID
|
||||
}
|
||||
|
||||
export class TranscriptionJobHandler extends AbstractJobHandler<CreateOptions, RunnerJobUpdatePayload, TranscriptionSuccess> {
|
||||
|
||||
protected isAbortSupported () {
|
||||
return true
|
||||
}
|
||||
|
||||
protected specificUpdate (_options: {
|
||||
runnerJob: MRunnerJob
|
||||
}) {
|
||||
// empty
|
||||
}
|
||||
|
||||
protected specificAbort (_options: {
|
||||
runnerJob: MRunnerJob
|
||||
}) {
|
||||
// empty
|
||||
}
|
||||
|
||||
protected async specificError (options: {
|
||||
runnerJob: MRunnerJob
|
||||
nextState: RunnerJobStateType
|
||||
}) {
|
||||
if (options.nextState !== RunnerJobState.ERRORED) return
|
||||
|
||||
await VideoJobInfoModel.decrease(options.runnerJob.privatePayload.videoUUID, 'pendingTranscription')
|
||||
}
|
||||
|
||||
protected async specificCancel (options: {
|
||||
runnerJob: MRunnerJob
|
||||
}) {
|
||||
await VideoJobInfoModel.decrease(options.runnerJob.privatePayload.videoUUID, 'pendingTranscription')
|
||||
}
|
||||
|
||||
async create (options: CreateOptions) {
|
||||
const { video } = options
|
||||
|
||||
const jobUUID = buildUUID()
|
||||
const payload: RunnerJobTranscriptionPayload = {
|
||||
input: {
|
||||
videoFileUrl: generateRunnerTranscodingVideoInputFileUrl(jobUUID, video.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
const privatePayload: RunnerJobTranscriptionPrivatePayload = {
|
||||
videoUUID: video.uuid
|
||||
}
|
||||
|
||||
const job = await this.createRunnerJob({
|
||||
type: 'video-transcription',
|
||||
jobUUID,
|
||||
payload,
|
||||
privatePayload,
|
||||
priority: JOB_PRIORITY.TRANSCODING
|
||||
})
|
||||
|
||||
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscription')
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
protected async specificComplete (options: {
|
||||
runnerJob: MRunnerJob
|
||||
resultPayload: TranscriptionSuccess
|
||||
}) {
|
||||
const { runnerJob, resultPayload } = options
|
||||
|
||||
const video = await loadRunnerVideo(runnerJob, this.lTags)
|
||||
if (!video) return
|
||||
|
||||
await onTranscriptionEnded({
|
||||
video,
|
||||
language: resultPayload.inputLanguage,
|
||||
vttPath: resultPayload.vttFile as string,
|
||||
lTags: this.lTags().tags
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { basename } from 'path'
|
||||
import { generateRunnerEditionTranscodingVideoInputFileUrl, generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls.js'
|
||||
import { AbstractJobHandler } from './abstract-job-handler.js'
|
||||
import { loadTranscodingRunnerVideo } from './shared/index.js'
|
||||
import { loadRunnerVideo } from './shared/utils.js'
|
||||
|
||||
type CreateOptions = {
|
||||
video: MVideo
|
||||
@@ -108,7 +108,7 @@ export class VideoStudioTranscodingJobHandler extends AbstractJobHandler<CreateO
|
||||
const { runnerJob, resultPayload } = options
|
||||
const privatePayload = runnerJob.privatePayload as RunnerJobVideoStudioTranscodingPrivatePayload
|
||||
|
||||
const video = await loadTranscodingRunnerVideo(runnerJob, this.lTags)
|
||||
const video = await loadRunnerVideo(runnerJob, this.lTags)
|
||||
if (!video) {
|
||||
await safeCleanupStudioTMPFiles(privatePayload.originalTasks)
|
||||
|
||||
@@ -149,7 +149,7 @@ export class VideoStudioTranscodingJobHandler extends AbstractJobHandler<CreateO
|
||||
const payload = runnerJob.privatePayload as RunnerJobVideoStudioTranscodingPrivatePayload
|
||||
await safeCleanupStudioTMPFiles(payload.originalTasks)
|
||||
|
||||
const video = await loadTranscodingRunnerVideo(options.runnerJob, this.lTags)
|
||||
const video = await loadRunnerVideo(options.runnerJob, this.lTags)
|
||||
if (!video) return
|
||||
|
||||
return video.setNewState(VideoState.PUBLISHED, false, undefined)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MVideo } from '@server/types/models/index.js'
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
RunnerJobUpdatePayload,
|
||||
RunnerJobVODAudioMergeTranscodingPayload,
|
||||
RunnerJobVODWebVideoTranscodingPrivatePayload,
|
||||
VODAudioMergeTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MVideo } from '@server/types/models/index.js'
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { generateRunnerTranscodingVideoInputFileUrl, generateRunnerTranscodingVideoPreviewFileUrl } from '../runner-urls.js'
|
||||
import { AbstractVODTranscodingJobHandler } from './abstract-vod-transcoding-job-handler.js'
|
||||
import { loadTranscodingRunnerVideo, onVODWebVideoOrAudioMergeTranscodingJob } from './shared/index.js'
|
||||
import { loadRunnerVideo, onVODWebVideoOrAudioMergeTranscodingJob } from './shared/utils.js'
|
||||
|
||||
type CreateOptions = {
|
||||
video: MVideo
|
||||
@@ -71,7 +71,7 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo
|
||||
const { runnerJob, resultPayload } = options
|
||||
const privatePayload = runnerJob.privatePayload as RunnerJobVODWebVideoTranscodingPrivatePayload
|
||||
|
||||
const video = await loadTranscodingRunnerVideo(runnerJob, this.lTags)
|
||||
const video = await loadRunnerVideo(runnerJob, this.lTags)
|
||||
if (!video) return
|
||||
|
||||
const videoFilePath = resultPayload.videoFile as string
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
RunnerJobUpdatePayload,
|
||||
RunnerJobVODHLSTranscodingPayload,
|
||||
RunnerJobVODHLSTranscodingPrivatePayload,
|
||||
VODHLSTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
|
||||
import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding.js'
|
||||
@@ -5,17 +13,9 @@ import { removeAllWebVideoFiles } from '@server/lib/video-file.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MVideo } from '@server/types/models/index.js'
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
RunnerJobUpdatePayload,
|
||||
RunnerJobVODHLSTranscodingPayload,
|
||||
RunnerJobVODHLSTranscodingPrivatePayload,
|
||||
VODHLSTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls.js'
|
||||
import { AbstractVODTranscodingJobHandler } from './abstract-vod-transcoding-job-handler.js'
|
||||
import { loadTranscodingRunnerVideo } from './shared/index.js'
|
||||
import { loadRunnerVideo } from './shared/utils.js'
|
||||
|
||||
type CreateOptions = {
|
||||
video: MVideo
|
||||
@@ -74,7 +74,7 @@ export class VODHLSTranscodingJobHandler extends AbstractVODTranscodingJobHandle
|
||||
const { runnerJob, resultPayload } = options
|
||||
const privatePayload = runnerJob.privatePayload as RunnerJobVODHLSTranscodingPrivatePayload
|
||||
|
||||
const video = await loadTranscodingRunnerVideo(runnerJob, this.lTags)
|
||||
const video = await loadRunnerVideo(runnerJob, this.lTags)
|
||||
if (!video) return
|
||||
|
||||
const videoFilePath = resultPayload.videoFile as string
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MVideo } from '@server/types/models/index.js'
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
RunnerJobUpdatePayload,
|
||||
RunnerJobVODWebVideoTranscodingPayload,
|
||||
RunnerJobVODWebVideoTranscodingPrivatePayload,
|
||||
VODWebVideoTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MVideo } from '@server/types/models/index.js'
|
||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||
import { generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls.js'
|
||||
import { AbstractVODTranscodingJobHandler } from './abstract-vod-transcoding-job-handler.js'
|
||||
import { loadTranscodingRunnerVideo, onVODWebVideoOrAudioMergeTranscodingJob } from './shared/index.js'
|
||||
import { loadRunnerVideo, onVODWebVideoOrAudioMergeTranscodingJob } from './shared/utils.js'
|
||||
|
||||
type CreateOptions = {
|
||||
video: MVideo
|
||||
@@ -70,7 +70,7 @@ export class VODWebVideoTranscodingJobHandler extends AbstractVODTranscodingJobH
|
||||
const { runnerJob, resultPayload } = options
|
||||
const privatePayload = runnerJob.privatePayload as RunnerJobVODWebVideoTranscodingPrivatePayload
|
||||
|
||||
const video = await loadTranscodingRunnerVideo(runnerJob, this.lTags)
|
||||
const video = await loadRunnerVideo(runnerJob, this.lTags)
|
||||
if (!video) return
|
||||
|
||||
const videoFilePath = resultPayload.videoFile as string
|
||||
|
||||
@@ -192,6 +192,9 @@ class ServerConfigManager {
|
||||
enabled: CONFIG.VIDEO_FILE.UPDATE.ENABLED
|
||||
}
|
||||
},
|
||||
videoTranscription: {
|
||||
enabled: CONFIG.VIDEO_TRANSCRIPTION.ENABLED
|
||||
},
|
||||
import: {
|
||||
videos: {
|
||||
http: {
|
||||
|
||||
@@ -93,7 +93,8 @@ export class UserSettingsImporter extends AbstractUserImporter <UserSettingsExpo
|
||||
autoInstanceFollowing: settingsImportData.autoInstanceFollowing,
|
||||
newPeerTubeVersion: settingsImportData.newPeerTubeVersion,
|
||||
newPluginVersion: settingsImportData.newPluginVersion,
|
||||
myVideoStudioEditionFinished: settingsImportData.myVideoStudioEditionFinished
|
||||
myVideoStudioEditionFinished: settingsImportData.myVideoStudioEditionFinished,
|
||||
myVideoTranscriptionGenerated: settingsImportData.myVideoTranscriptionGenerated
|
||||
}
|
||||
|
||||
await UserNotificationSettingModel.updateUserSettings(values, this.user.id)
|
||||
|
||||
@@ -274,7 +274,8 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
|
||||
autoInstanceFollowing: UserNotificationSettingValue.WEB,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB,
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB,
|
||||
myVideoTranscriptionGenerated: UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
return UserNotificationSettingModel.create(values, { transaction: t })
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import { hasAudioStream } from '@peertube/peertube-ffmpeg'
|
||||
import { buildSUUID } from '@peertube/peertube-node-utils'
|
||||
import { AbstractTranscriber, TranscriptionModel, WhisperBuiltinModel, transcriberFactory } from '@peertube/peertube-transcription'
|
||||
import { moveAndProcessCaptionFile } from '@server/helpers/captions-utils.js'
|
||||
import { isVideoCaptionLanguageValid } from '@server/helpers/custom-validators/video-captions.js'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { DIRECTORIES } from '@server/initializers/constants.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { MVideo, MVideoCaption } from '@server/types/models/index.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MVideo, MVideoCaption, MVideoFullLight, MVideoUUID, MVideoUrl } from '@server/types/models/index.js'
|
||||
import { ensureDir, remove } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { federateVideoIfNeeded } from './activitypub/videos/federate.js'
|
||||
import { JobQueue } from './job-queue/job-queue.js'
|
||||
import { Notifier } from './notifier/notifier.js'
|
||||
import { TranscriptionJobHandler } from './runners/index.js'
|
||||
import { VideoPathManager } from './video-path-manager.js'
|
||||
|
||||
const lTags = loggerTagsFactory('video-caption')
|
||||
|
||||
export async function createLocalCaption (options: {
|
||||
video: MVideo
|
||||
@@ -22,5 +40,122 @@ export async function createLocalCaption (options: {
|
||||
await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t)
|
||||
})
|
||||
|
||||
return videoCaption
|
||||
return Object.assign(videoCaption, { Video: video })
|
||||
}
|
||||
|
||||
export async function createTranscriptionTaskIfNeeded (video: MVideoUUID & MVideoUrl) {
|
||||
if (CONFIG.VIDEO_TRANSCRIPTION.ENABLED !== true) return
|
||||
|
||||
logger.info(`Creating transcription job for ${video.url}`, lTags(video.uuid))
|
||||
|
||||
if (CONFIG.VIDEO_TRANSCRIPTION.REMOTE_RUNNERS.ENABLED === true) {
|
||||
await new TranscriptionJobHandler().create({ video })
|
||||
} else {
|
||||
await JobQueue.Instance.createJob({ type: 'video-transcription', payload: { videoUUID: video.uuid } })
|
||||
}
|
||||
|
||||
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscription')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transcription task
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let transcriber: AbstractTranscriber
|
||||
|
||||
export async function generateSubtitle (options: {
|
||||
video: MVideoUUID
|
||||
}) {
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(options.video.uuid)
|
||||
|
||||
const outputPath = join(CONFIG.STORAGE.TMP_DIR, 'transcription', buildSUUID())
|
||||
await ensureDir(outputPath)
|
||||
|
||||
const binDirectory = join(DIRECTORIES.LOCAL_PIP_DIRECTORY, 'bin')
|
||||
|
||||
try {
|
||||
// Lazy load the transcriber
|
||||
if (!transcriber) {
|
||||
transcriber = transcriberFactory.createFromEngineName({
|
||||
engineName: CONFIG.VIDEO_TRANSCRIPTION.ENGINE,
|
||||
enginePath: CONFIG.VIDEO_TRANSCRIPTION.ENGINE_PATH,
|
||||
logger,
|
||||
binDirectory
|
||||
})
|
||||
|
||||
if (!CONFIG.VIDEO_TRANSCRIPTION.ENGINE_PATH) {
|
||||
logger.info(`Installing transcriber ${transcriber.engine.name} to generate subtitles`, lTags())
|
||||
await transcriber.install(DIRECTORIES.LOCAL_PIP_DIRECTORY)
|
||||
}
|
||||
}
|
||||
|
||||
const video = await VideoModel.loadFull(options.video.uuid)
|
||||
const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
|
||||
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
|
||||
if (await hasAudioStream(videoInputPath) !== true) {
|
||||
logger.info(
|
||||
`Do not run transcription for ${video.uuid} in ${outputPath} because it does not contain an audio stream`,
|
||||
lTags(video.uuid)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(`Running transcription for ${video.uuid} in ${outputPath}`, lTags(video.uuid))
|
||||
|
||||
const transcriptFile = await transcriber.transcribe({
|
||||
mediaFilePath: videoInputPath,
|
||||
|
||||
model: CONFIG.VIDEO_TRANSCRIPTION.MODEL_PATH
|
||||
? await TranscriptionModel.fromPath(CONFIG.VIDEO_TRANSCRIPTION.MODEL_PATH)
|
||||
: new WhisperBuiltinModel(CONFIG.VIDEO_TRANSCRIPTION.MODEL),
|
||||
|
||||
transcriptDirectory: outputPath,
|
||||
|
||||
format: 'vtt'
|
||||
})
|
||||
|
||||
await onTranscriptionEnded({ video, language: transcriptFile.language, vttPath: transcriptFile.path })
|
||||
})
|
||||
} finally {
|
||||
if (outputPath) await remove(outputPath)
|
||||
|
||||
inputFileMutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
export async function onTranscriptionEnded (options: {
|
||||
video: MVideoFullLight
|
||||
language: string
|
||||
vttPath: string
|
||||
lTags?: (string | number)[]
|
||||
}) {
|
||||
const { video, language, vttPath, lTags: customLTags = [] } = options
|
||||
|
||||
await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscription')
|
||||
|
||||
if (!isVideoCaptionLanguageValid(language)) {
|
||||
logger.warn(`Invalid transcription language for video ${video.uuid}`, this.lTags(video.uuid))
|
||||
return
|
||||
}
|
||||
|
||||
if (!video.language) {
|
||||
video.language = language
|
||||
await video.save()
|
||||
}
|
||||
|
||||
const caption = await createLocalCaption({
|
||||
video,
|
||||
language,
|
||||
path: vttPath
|
||||
})
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
await federateVideoIfNeeded(video, false, t)
|
||||
})
|
||||
|
||||
Notifier.Instance.notifyOfGeneratedVideoTranscription(caption)
|
||||
|
||||
logger.info(`Transcription ended for ${video.uuid}`, lTags(video.uuid, ...customLTags))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||
import { MVideo, MVideoFile, MVideoId, MVideoWithAllFiles } from '@server/types/models/index.js'
|
||||
import { FfprobeData } from 'fluent-ffmpeg'
|
||||
import { move, remove } from 'fs-extra'
|
||||
import { move, remove } from 'fs-extra/esm'
|
||||
import { lTags } from './object-storage/shared/index.js'
|
||||
import { storeOriginalVideoFile } from './object-storage/videos.js'
|
||||
import { generateHLSVideoFilename, generateWebVideoFilename } from './paths.js'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MVideo, MVideoFile, MVideoFullLight, MVideoUUID } from '@server/types/models/index.js'
|
||||
import { CreateJobArgument, CreateJobOptions, JobQueue } from './job-queue/job-queue.js'
|
||||
import { createTranscriptionTaskIfNeeded } from './video-captions.js'
|
||||
import { moveFilesIfPrivacyChanged } from './video-privacy.js'
|
||||
|
||||
export async function buildMoveJob (options: {
|
||||
@@ -57,8 +58,9 @@ export function buildStoryboardJobIfNeeded (options: {
|
||||
export async function addVideoJobsAfterCreation (options: {
|
||||
video: MVideo
|
||||
videoFile: MVideoFile
|
||||
generateTranscription: boolean
|
||||
}) {
|
||||
const { video, videoFile } = options
|
||||
const { video, videoFile, generateTranscription } = options
|
||||
|
||||
const jobs: (CreateJobArgument & CreateJobOptions)[] = [
|
||||
{
|
||||
@@ -105,7 +107,11 @@ export async function addVideoJobsAfterCreation (options: {
|
||||
})
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createSequentialJobFlow(...jobs)
|
||||
await JobQueue.Instance.createSequentialJobFlow(...jobs)
|
||||
|
||||
if (generateTranscription === true && CONFIG.VIDEO_TRANSCRIPTION.ENABLED === true) {
|
||||
await createTranscriptionTaskIfNeeded(video)
|
||||
}
|
||||
}
|
||||
|
||||
export async function addVideoJobsAfterUpdate (options: {
|
||||
|
||||
@@ -259,6 +259,7 @@ async function buildYoutubeDLImport (options: {
|
||||
type: 'youtube-dl' as 'youtube-dl',
|
||||
videoImportId: videoImport.id,
|
||||
fileExt,
|
||||
generateTranscription: importDataOverride.generateTranscription ?? true,
|
||||
// If part of a sync process, there is a parent job that will aggregate children results
|
||||
preventException: !!channelSync
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, VideoCaptionGenerate } from '@peertube/peertube-models'
|
||||
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
|
||||
@@ -14,7 +18,7 @@ import {
|
||||
isValidVideoPasswordHeader
|
||||
} from '../shared/index.js'
|
||||
|
||||
const addVideoCaptionValidator = [
|
||||
export const addVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('captionLanguage')
|
||||
@@ -41,7 +45,75 @@ const addVideoCaptionValidator = [
|
||||
}
|
||||
]
|
||||
|
||||
const deleteVideoCaptionValidator = [
|
||||
export const generateVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('forceTranscription')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.VIDEO_TRANSCRIPTION.ENABLED !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Video transcription is disabled on this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (video.remote) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot run transcription job on a remote video'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the user who did the request is able to update the video
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return
|
||||
|
||||
// Check the video has not already a caption
|
||||
const captions = await VideoCaptionModel.listVideoCaptions(video.id)
|
||||
if (captions.length !== 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This video already has captions'
|
||||
})
|
||||
}
|
||||
|
||||
// Bypass "video is already transcribed" check
|
||||
const body = req.body as VideoCaptionGenerate
|
||||
if (body.forceTranscription === true) {
|
||||
if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only admins can force transcription'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
const info = await VideoJobInfoModel.load(video.id)
|
||||
if (info && info.pendingTranscription > 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
type: ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCRIBED,
|
||||
message: 'This video is already being transcribed'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('captionLanguage')
|
||||
@@ -60,7 +132,7 @@ const deleteVideoCaptionValidator = [
|
||||
}
|
||||
]
|
||||
|
||||
const listVideoCaptionsValidator = [
|
||||
export const listVideoCaptionsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
@@ -75,9 +147,3 @@ const listVideoCaptionsValidator = [
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
addVideoCaptionValidator,
|
||||
listVideoCaptionsValidator,
|
||||
deleteVideoCaptionValidator
|
||||
}
|
||||
|
||||
@@ -183,7 +183,12 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery {
|
||||
"Account->Actor->Server"."id" AS "Account.Actor.Server.id",
|
||||
"Account->Actor->Server"."host" AS "Account.Actor.Server.host",
|
||||
"UserRegistration"."id" AS "UserRegistration.id",
|
||||
"UserRegistration"."username" AS "UserRegistration.username"`
|
||||
"UserRegistration"."username" AS "UserRegistration.username",
|
||||
"VideoCaption"."id" AS "VideoCaption.id",
|
||||
"VideoCaption"."language" AS "VideoCaption.language",
|
||||
"VideoCaption->Video"."id" AS "VideoCaption.Video.id",
|
||||
"VideoCaption->Video"."uuid" AS "VideoCaption.Video.uuid",
|
||||
"VideoCaption->Video"."name" AS "VideoCaption.Video.name"`
|
||||
}
|
||||
|
||||
private getJoins () {
|
||||
@@ -269,6 +274,11 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery {
|
||||
LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"
|
||||
) ON "UserNotificationModel"."accountId" = "Account"."id"
|
||||
|
||||
LEFT JOIN "userRegistration" as "UserRegistration" ON "UserNotificationModel"."userRegistrationId" = "UserRegistration"."id"`
|
||||
LEFT JOIN "userRegistration" as "UserRegistration" ON "UserNotificationModel"."userRegistrationId" = "UserRegistration"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"videoCaption" AS "VideoCaption"
|
||||
INNER JOIN "video" AS "VideoCaption->Video" ON "VideoCaption"."videoId" = "VideoCaption->Video"."id"
|
||||
) ON "UserNotificationModel"."videoCaptionId" = "VideoCaption"."id"`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +181,15 @@ export class UserNotificationSettingModel extends SequelizeModel<UserNotificatio
|
||||
@Column
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingTranscriptionGeneratedForOwner',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoTranscriptionGenerated')
|
||||
)
|
||||
@Column
|
||||
myVideoTranscriptionGenerated: UserNotificationSettingValueType
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
@@ -233,6 +242,7 @@ export class UserNotificationSettingModel extends SequelizeModel<UserNotificatio
|
||||
abuseStateChange: this.abuseStateChange,
|
||||
newPeerTubeVersion: this.newPeerTubeVersion,
|
||||
myVideoStudioEditionFinished: this.myVideoStudioEditionFinished,
|
||||
myVideoTranscriptionGenerated: this.myVideoTranscriptionGenerated,
|
||||
newPluginVersion: this.newPluginVersion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ApplicationModel } from '../application/application.js'
|
||||
import { PluginModel } from '../server/plugin.js'
|
||||
import { SequelizeModel, throwIfNotValid } from '../shared/index.js'
|
||||
import { VideoBlacklistModel } from '../video/video-blacklist.js'
|
||||
import { VideoCaptionModel } from '../video/video-caption.js'
|
||||
import { VideoCommentModel } from '../video/video-comment.js'
|
||||
import { VideoImportModel } from '../video/video-import.js'
|
||||
import { VideoModel } from '../video/video.js'
|
||||
@@ -260,6 +261,18 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
|
||||
})
|
||||
UserRegistration: Awaited<UserRegistrationModel>
|
||||
|
||||
@ForeignKey(() => VideoCaptionModel)
|
||||
@Column
|
||||
videoCaptionId: number
|
||||
|
||||
@BelongsTo(() => VideoCaptionModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
VideoCaption: Awaited<VideoCaptionModel>
|
||||
|
||||
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
|
||||
const where = { userId }
|
||||
|
||||
@@ -441,6 +454,17 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
|
||||
? { id: this.UserRegistration.id, username: this.UserRegistration.username }
|
||||
: undefined
|
||||
|
||||
const videoCaption = this.VideoCaption
|
||||
? {
|
||||
id: this.VideoCaption.id,
|
||||
language: {
|
||||
id: this.VideoCaption.language,
|
||||
label: VideoCaptionModel.getLanguageLabel(this.VideoCaption.language)
|
||||
},
|
||||
video: this.formatVideo(this.VideoCaption.Video)
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
@@ -455,6 +479,7 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
|
||||
plugin,
|
||||
peertube,
|
||||
registration,
|
||||
videoCaption,
|
||||
createdAt: this.createdAt.toISOString(),
|
||||
updatedAt: this.updatedAt.toISOString()
|
||||
}
|
||||
@@ -531,4 +556,11 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
|
||||
width: a.width
|
||||
}
|
||||
}
|
||||
|
||||
formatVideoCaption (a: UserNotificationIncludes.ActorImageInclude) {
|
||||
return {
|
||||
path: a.getStaticPath(),
|
||||
width: a.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,10 +112,21 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
||||
return undefined
|
||||
}
|
||||
|
||||
static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) {
|
||||
const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language, transaction)
|
||||
|
||||
// Delete existing file
|
||||
if (existing) await existing.destroy({ transaction })
|
||||
|
||||
return caption.save({ transaction })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static loadByVideoIdAndLanguage (videoId: string | number, language: string, transaction?: Transaction): Promise<MVideoCaptionVideo> {
|
||||
const videoInclude = {
|
||||
model: VideoModel.unscoped(),
|
||||
attributes: [ 'id', 'remote', 'uuid' ],
|
||||
attributes: [ 'id', 'name', 'remote', 'uuid', 'url' ],
|
||||
where: buildWhereIdOrUUID(videoId)
|
||||
}
|
||||
|
||||
@@ -148,13 +159,18 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
||||
return VideoCaptionModel.findOne(query)
|
||||
}
|
||||
|
||||
static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) {
|
||||
const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language, transaction)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Delete existing file
|
||||
if (existing) await existing.destroy({ transaction })
|
||||
static async hasVideoCaption (videoId: number) {
|
||||
const query = {
|
||||
where: {
|
||||
videoId
|
||||
}
|
||||
}
|
||||
|
||||
return caption.save({ transaction })
|
||||
const result = await VideoCaptionModel.unscoped().findOne(query)
|
||||
|
||||
return !!result
|
||||
}
|
||||
|
||||
static listVideoCaptions (videoId: number, transaction?: Transaction): Promise<MVideoCaptionVideo[]> {
|
||||
@@ -194,29 +210,16 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
||||
return result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static getLanguageLabel (language: string) {
|
||||
return VIDEO_LANGUAGES[language] || 'Unknown'
|
||||
}
|
||||
|
||||
static deleteAllCaptionsOfRemoteVideo (videoId: number, transaction: Transaction) {
|
||||
const query = {
|
||||
where: {
|
||||
videoId
|
||||
},
|
||||
transaction
|
||||
}
|
||||
|
||||
return VideoCaptionModel.destroy(query)
|
||||
}
|
||||
|
||||
static generateCaptionName (language: string) {
|
||||
return `${buildUUID()}-${language}.vtt`
|
||||
}
|
||||
|
||||
isOwned () {
|
||||
return this.Video.remote === false
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
|
||||
@@ -240,6 +243,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isOwned () {
|
||||
return this.Video.remote === false
|
||||
}
|
||||
|
||||
getCaptionStaticPath (this: MVideoCaptionLanguageUrl) {
|
||||
return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { Op, QueryTypes, Transaction } from 'sequelize'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, IsInt, Table, Unique, UpdatedAt } from 'sequelize-typescript'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { VideoModel } from './video.js'
|
||||
import { SequelizeModel } from '../shared/sequelize-type.js'
|
||||
import { VideoModel } from './video.js'
|
||||
|
||||
export type VideoJobInfoColumnType = 'pendingMove' | 'pendingTranscode'
|
||||
export type VideoJobInfoColumnType = 'pendingMove' | 'pendingTranscode' | 'pendingTranscription'
|
||||
|
||||
@Table({
|
||||
tableName: 'videoJobInfo',
|
||||
@@ -39,6 +39,12 @@ export class VideoJobInfoModel extends SequelizeModel<VideoJobInfoModel> {
|
||||
@Column
|
||||
pendingTranscode: number
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(0)
|
||||
@IsInt
|
||||
@Column
|
||||
pendingTranscription: number
|
||||
|
||||
@ForeignKey(() => VideoModel)
|
||||
@Unique
|
||||
@Column
|
||||
|
||||
@@ -16,6 +16,7 @@ import { VideoBlacklistModel } from '../../../models/video/video-blacklist.js'
|
||||
import { VideoChannelModel } from '../../../models/video/video-channel.js'
|
||||
import { VideoCommentModel } from '../../../models/video/video-comment.js'
|
||||
import { VideoImportModel } from '../../../models/video/video-import.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
|
||||
type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M>
|
||||
|
||||
@@ -98,13 +99,17 @@ export module UserNotificationIncludes {
|
||||
|
||||
export type UserRegistrationInclude =
|
||||
Pick<UserRegistrationModel, 'id' | 'username'>
|
||||
|
||||
export type VideoCaptionInclude =
|
||||
Pick<VideoCaptionModel, 'id' | 'language'> &
|
||||
PickWith<VideoCaptionModel, 'Video', VideoInclude>
|
||||
}
|
||||
|
||||
// ############################################################################
|
||||
|
||||
export type MUserNotification =
|
||||
Omit<UserNotificationModel, 'User' | 'Video' | 'VideoComment' | 'Abuse' | 'VideoBlacklist' |
|
||||
'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application' | 'UserRegistration'>
|
||||
'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application' | 'UserRegistration' | 'VideoCaption'>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
@@ -119,4 +124,5 @@ export type UserNotificationModelForApi =
|
||||
Use<'Plugin', UserNotificationIncludes.PluginInclude> &
|
||||
Use<'Application', UserNotificationIncludes.ApplicationInclude> &
|
||||
Use<'Account', UserNotificationIncludes.AccountIncludeActor> &
|
||||
Use<'UserRegistration', UserNotificationIncludes.UserRegistrationInclude>
|
||||
Use<'UserRegistration', UserNotificationIncludes.UserRegistrationInclude> &
|
||||
Use<'VideoCaption', UserNotificationIncludes.VideoCaptionInclude>
|
||||
|
||||
@@ -16,7 +16,7 @@ export type MVideoCaptionLanguageUrl =
|
||||
|
||||
export type MVideoCaptionVideo =
|
||||
MVideoCaption &
|
||||
Use<'Video', Pick<MVideo, 'id' | 'remote' | 'uuid'>>
|
||||
Use<'Video', Pick<MVideo, 'id' | 'name' | 'remote' | 'uuid' | 'url' | 'getWatchStaticPath'>>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
{ "path": "../packages/models" },
|
||||
{ "path": "../packages/node-utils" },
|
||||
{ "path": "../packages/server-commands" },
|
||||
{ "path": "../packages/typescript-utils" }
|
||||
{ "path": "../packages/typescript-utils" },
|
||||
{ "path": "../packages/transcription" }
|
||||
],
|
||||
"exclude": [
|
||||
"tests/"
|
||||
|
||||
Reference in New Issue
Block a user