Merge branch 'master' into PHRAS-3169_Prod_video_tools_vtt_with_number

This commit is contained in:
Nicolas Maillat
2020-07-09 15:51:36 +02:00
committed by GitHub
19 changed files with 670 additions and 21 deletions

View File

@@ -328,6 +328,14 @@ workers:
password: guest password: guest
vhost: / vhost: /
externalservice:
ginger:
AutoSubtitling:
service_base_url: https://base.uri
token: 39c6011d
transcript_format: text/vtt
subdef_source: preview
user_account: user_account:
deleting_policies: deleting_policies:
email_confirmation: true email_confirmation: true

View File

@@ -91,7 +91,6 @@ use Alchemy\Phrasea\WorkerManager\Provider\AlchemyWorkerServiceProvider;
use Alchemy\Phrasea\WorkerManager\Provider\QueueWorkerServiceProvider; use Alchemy\Phrasea\WorkerManager\Provider\QueueWorkerServiceProvider;
use Alchemy\QueueProvider\QueueServiceProvider; use Alchemy\QueueProvider\QueueServiceProvider;
use Alchemy\WorkerProvider\WorkerServiceProvider; use Alchemy\WorkerProvider\WorkerServiceProvider;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use MediaVorus\Media\MediaInterface; use MediaVorus\Media\MediaInterface;
use MediaVorus\MediaVorus; use MediaVorus\MediaVorus;
use Monolog\Handler\ErrorLogHandler; use Monolog\Handler\ErrorLogHandler;

View File

@@ -15,8 +15,10 @@ use Alchemy\Phrasea\Application\Helper\FilesystemAware;
use Alchemy\Phrasea\Application\Helper\SubDefinitionSubstituerAware; use Alchemy\Phrasea\Application\Helper\SubDefinitionSubstituerAware;
use Alchemy\Phrasea\Controller\Controller; use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Controller\RecordsRequest; use Alchemy\Phrasea\Controller\RecordsRequest;
use Alchemy\Phrasea\Core\Event\Record\RecordAutoSubtitleEvent;
use Alchemy\Phrasea\Core\Event\Record\RecordEvents; use Alchemy\Phrasea\Core\Event\Record\RecordEvents;
use Alchemy\Phrasea\Core\Event\Record\SubdefinitionCreateEvent; use Alchemy\Phrasea\Core\Event\Record\SubdefinitionCreateEvent;
use Alchemy\Phrasea\Core\PhraseaEvents;
use Alchemy\Phrasea\Exception\RuntimeException; use Alchemy\Phrasea\Exception\RuntimeException;
use Alchemy\Phrasea\Metadata\PhraseanetMetadataReader; use Alchemy\Phrasea\Metadata\PhraseanetMetadataReader;
use Alchemy\Phrasea\Metadata\PhraseanetMetadataSetter; use Alchemy\Phrasea\Metadata\PhraseanetMetadataSetter;
@@ -24,7 +26,6 @@ use Alchemy\Phrasea\Record\RecordWasRotated;
use DataURI\Parser; use DataURI\Parser;
use MediaAlchemyst\Alchemyst; use MediaAlchemyst\Alchemyst;
use MediaVorus\MediaVorus; use MediaVorus\MediaVorus;
use PHPExiftool\Reader;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class ToolsController extends Controller class ToolsController extends Controller
@@ -45,7 +46,6 @@ class ToolsController extends Controller
if (count($records) == 1) { if (count($records) == 1) {
/** @var \record_adapter $record */ /** @var \record_adapter $record */
$record = $records->first(); $record = $records->first();
$databox = $record->getDatabox();
/**Array list of subdefs**/ /**Array list of subdefs**/
$listsubdef = array_keys($record-> get_subdefs()); $listsubdef = array_keys($record-> get_subdefs());
@@ -88,14 +88,13 @@ class ToolsController extends Controller
$metadatas = true; $metadatas = true;
} }
} }
$conf = $this->getConf();
return $this->render('prod/actions/Tools/index.html.twig', [ return $this->render('prod/actions/Tools/index.html.twig', [
'records' => $records, 'records' => $records,
'record' => $record, 'record' => $record,
'recordSubdefs' => $recordAccessibleSubdefs, 'recordSubdefs' => $recordAccessibleSubdefs,
'metadatas' => $metadatas, 'metadatas' => $metadatas,
'listsubdef' => $listsubdef 'listsubdef' => $listsubdef
]); ]);
} }
@@ -118,6 +117,7 @@ class ToolsController extends Controller
} }
foreach ($records as $record) { foreach ($records as $record) {
/** @var \media_subdef $subdef */
foreach ($record->get_subdefs() as $subdef) { foreach ($record->get_subdefs() as $subdef) {
if ($subdef->get_type() !== \media_subdef::TYPE_IMAGE) { if ($subdef->get_type() !== \media_subdef::TYPE_IMAGE) {
continue; continue;
@@ -146,6 +146,7 @@ class ToolsController extends Controller
foreach ($selection as $record) { foreach ($selection as $record) {
$substituted = false; $substituted = false;
/** @var \media_subdef $subdef */
foreach ($record->get_subdefs() as $subdef) { foreach ($record->get_subdefs() as $subdef) {
if ($subdef->is_substituted()) { if ($subdef->is_substituted()) {
$substituted = true; $substituted = true;
@@ -362,14 +363,6 @@ class ToolsController extends Controller
return $this->app->json($return); return $this->app->json($return);
} }
/**
* @return Reader
*/
private function getExifToolReader()
{
return $this->app['exiftool.reader'];
}
/** /**
* @return Alchemyst * @return Alchemyst
*/ */
@@ -449,13 +442,38 @@ class ToolsController extends Controller
try { try {
$record->set_metadatas($metadatas); $record->set_metadatas($metadatas);
} }
catch (Exception $e) { catch (\Exception $e) {
return $this->app->json(['success' => false, 'errorMessage' => $e->getMessage()]); return $this->app->json(['success' => false, 'errorMessage' => $e->getMessage()]);
} }
return $this->app->json(['success' => true, 'errorMessage' => '']); return $this->app->json(['success' => true, 'errorMessage' => '']);
} }
public function autoSubtitleAction(Request $request)
{
$record = new \record_adapter($this->app,
(int)$request->request->get("databox_id"),
(int)$request->request->get("record_id")
);
$permalinkUrl = '';
$conf = $this->getConf();
// if subdef_source not set, by default use the preview permalink
$subdefSource = $conf->get(['externalservice', 'ginger', 'AutoSubtitling', 'subdef_source']) ?: 'preview';
if ($this->isPhysicallyPresent($record, $subdefSource) && ($previewLink = $record->get_subdef($subdefSource)->get_permalink()) != null) {
$permalinkUrl = $previewLink->get_url()->__toString();
}
$this->dispatch(
PhraseaEvents::RECORD_AUTO_SUBTITLE,
new RecordAutoSubtitleEvent($record, $permalinkUrl, $request->request->get("subtitle_language_source"), $request->request->get("meta_struct_id"))
);
return $this->app->json(["status" => "dispatch"]);
}
public function videoEditorAction(Request $request) public function videoEditorAction(Request $request)
{ {
$records = RecordsRequest::fromRequest($this->app, $request, false); $records = RecordsRequest::fromRequest($this->app, $request, false);
@@ -491,7 +509,7 @@ class ToolsController extends Controller
$fieldValue = array_pop($fieldValues); $fieldValue = array_pop($fieldValues);
$field['value'] = $fieldValue->getValue(); $field['value'] = $fieldValue->getValue();
} }
$videoTextTrackFields[] = $field; $videoTextTrackFields[$meta->get_id()] = $field;
unset($field); unset($field);
} }
} }
@@ -511,4 +529,15 @@ class ToolsController extends Controller
'videoTextTrackFields' => $videoTextTrackFields 'videoTextTrackFields' => $videoTextTrackFields
]); ]);
} }
private function isPhysicallyPresent(\record_adapter $record, $subdefName)
{
try {
return $record->get_subdef($subdefName)->is_physically_present();
} catch (\Exception $e) {
unset($e);
}
return false;
}
} }

View File

@@ -72,6 +72,9 @@ class Tools implements ControllerProviderInterface, ServiceProviderInterface
$controllers->post('/metadata/save/', 'controller.prod.tools:saveMetasAction') $controllers->post('/metadata/save/', 'controller.prod.tools:saveMetasAction')
->bind('prod_tools_metadata_save'); ->bind('prod_tools_metadata_save');
$controllers->post('/auto-subtitle/', 'controller.prod.tools:autoSubtitleAction')
->bind('prod_tools_auto_subtitle');
$controllers->get('/videoEditor', 'controller.prod.tools:videoEditorAction'); $controllers->get('/videoEditor', 'controller.prod.tools:videoEditorAction');
return $controllers; return $controllers;

View File

@@ -0,0 +1,36 @@
<?php
namespace Alchemy\Phrasea\Core\Event\Record;
use Alchemy\Phrasea\Model\RecordInterface;
class RecordAutoSubtitleEvent extends RecordEvent
{
private $languageSource;
private $metaStructId;
private $permalinkUrl;
public function __construct(RecordInterface $record, $permalinkUrl, $languageSource, $metaStructId)
{
parent::__construct($record);
$this->languageSource = $languageSource;
$this->metaStructId = $metaStructId;
$this->permalinkUrl = $permalinkUrl;
}
public function getLanguageSource()
{
return $this->languageSource;
}
public function getMetaStructId()
{
return $this->metaStructId;
}
public function getPermalinkUrl()
{
return $this->permalinkUrl;
}
}

View File

@@ -54,6 +54,8 @@ final class PhraseaEvents
const RECORD_EDIT = 'record.edit'; const RECORD_EDIT = 'record.edit';
const RECORD_UPLOAD = 'record.upload'; const RECORD_UPLOAD = 'record.upload';
const RECORD_AUTO_SUBTITLE = 'record.auto-subtitle';
const THESAURUS_IMPORTED = 'thesaurus.imported'; const THESAURUS_IMPORTED = 'thesaurus.imported';
const THESAURUS_FIELD_LINKED = 'thesaurus.field-linked'; const THESAURUS_FIELD_LINKED = 'thesaurus.field-linked';
const THESAURUS_CANDIDATE_ACCEPTED_AS_CONCEPT = 'thesaurus.candidate-accepted-as-concept'; const THESAURUS_CANDIDATE_ACCEPTED_AS_CONCEPT = 'thesaurus.candidate-accepted-as-concept';

View File

@@ -150,6 +150,9 @@ class RepositoriesServiceProvider implements ServiceProviderInterface
$app['repo.worker-running-job'] = $app->share(function (PhraseaApplication $app) { $app['repo.worker-running-job'] = $app->share(function (PhraseaApplication $app) {
return $app['orm.em']->getRepository('Phraseanet:WorkerRunningJob'); return $app['orm.em']->getRepository('Phraseanet:WorkerRunningJob');
}); });
$app['repo.worker-job'] = $app->share(function (PhraseaApplication $app) {
return $app['orm.em']->getRepository('Phraseanet:WorkerJob');
});
$app['repo.worker-running-populate'] = $app->share(function (PhraseaApplication $app) { $app['repo.worker-running-populate'] = $app->share(function (PhraseaApplication $app) {
return $app['orm.em']->getRepository('Phraseanet:WorkerRunningPopulate'); return $app['orm.em']->getRepository('Phraseanet:WorkerRunningPopulate');
}); });

View File

@@ -16,7 +16,6 @@ class Version
/** /**
* @var string * @var string
*/ */
private $number = '4.1.1'; private $number = '4.1.1';
/** /**

View File

@@ -0,0 +1,150 @@
<?php
namespace Alchemy\Phrasea\Model\Entities;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ORM\Table(name="WorkerJob", indexes={@ORM\Index(name="worker_job_type", columns={"type"})})
* @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\WorkerJobRepository")
*/
class WorkerJob
{
const WAITING = "waiting";
const RUNNING = "running";
const FINISHED = "finished";
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @ORM\Column(type="string", name="type")
*/
private $type;
/**
* @ORM\Column(type="json_array", name="data", nullable=false)
*/
private $data;
/**
* @ORM\Column(type="string", name="status")
*/
private $status;
/**
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime")
*/
private $created;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $started;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $finished;
/**
* @return integer
*/
public function getId()
{
return $this->id;
}
public function setType($type)
{
$this->type = $type;
return $this;
}
public function getType()
{
return $this->type;
}
/**
* @param array $data
*
* @return WorkerJob
*/
public function setData(array $data)
{
$this->data = $data;
return $this;
}
public function getData()
{
return $this->data;
}
public function setStatus($status)
{
$this->status = $status;
return $this;
}
public function getStatus()
{
return $this->status;
}
/**
* @return \DateTime
*/
public function getCreated()
{
return $this->created;
}
/**
* @param \DateTime $finished
* @return $this
*/
public function setFinished(\DateTime $finished)
{
$this->finished = $finished;
return $this;
}
/**
* @return mixed
*/
public function getFinished()
{
return $this->finished;
}
/**
* @param \DateTime $started
* @return $this
*/
public function setStarted(\DateTime $started)
{
$this->started = $started;
return $this;
}
/**
* @return mixed
*/
public function getStarted()
{
return $this->started;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Alchemy\Phrasea\Model\Repositories;
use Doctrine\ORM\EntityRepository;
class WorkerJobRepository extends EntityRepository
{
public function getEntityManager()
{
return parent::getEntityManager();
}
public function reconnect()
{
if($this->_em->getConnection()->ping() === false) {
$this->_em->getConnection()->close();
$this->_em->getConnection()->connect();
}
}
}

View File

@@ -11,11 +11,13 @@ use Alchemy\Phrasea\WorkerManager\Worker\CreateRecordWorker;
use Alchemy\Phrasea\WorkerManager\Worker\DeleteRecordWorker; use Alchemy\Phrasea\WorkerManager\Worker\DeleteRecordWorker;
use Alchemy\Phrasea\WorkerManager\Worker\ExportMailWorker; use Alchemy\Phrasea\WorkerManager\Worker\ExportMailWorker;
use Alchemy\Phrasea\WorkerManager\Worker\Factory\CallableWorkerFactory; use Alchemy\Phrasea\WorkerManager\Worker\Factory\CallableWorkerFactory;
use Alchemy\Phrasea\WorkerManager\Worker\MainQueueWorker;
use Alchemy\Phrasea\WorkerManager\Worker\PopulateIndexWorker; use Alchemy\Phrasea\WorkerManager\Worker\PopulateIndexWorker;
use Alchemy\Phrasea\WorkerManager\Worker\ProcessPool; use Alchemy\Phrasea\WorkerManager\Worker\ProcessPool;
use Alchemy\Phrasea\WorkerManager\Worker\PullAssetsWorker; use Alchemy\Phrasea\WorkerManager\Worker\PullAssetsWorker;
use Alchemy\Phrasea\WorkerManager\Worker\Resolver\TypeBasedWorkerResolver; use Alchemy\Phrasea\WorkerManager\Worker\Resolver\TypeBasedWorkerResolver;
use Alchemy\Phrasea\WorkerManager\Worker\SubdefCreationWorker; use Alchemy\Phrasea\WorkerManager\Worker\SubdefCreationWorker;
use Alchemy\Phrasea\WorkerManager\Worker\SubtitleWorker;
use Alchemy\Phrasea\WorkerManager\Worker\WebhookWorker; use Alchemy\Phrasea\WorkerManager\Worker\WebhookWorker;
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInvoker; use Alchemy\Phrasea\WorkerManager\Worker\WorkerInvoker;
use Alchemy\Phrasea\WorkerManager\Worker\WriteMetadatasWorker; use Alchemy\Phrasea\WorkerManager\Worker\WriteMetadatasWorker;
@@ -128,6 +130,14 @@ class AlchemyWorkerServiceProvider implements PluginProviderInterface
return (new DeleteRecordWorker()) return (new DeleteRecordWorker())
->setApplicationBox($app['phraseanet.appbox']); ->setApplicationBox($app['phraseanet.appbox']);
})); }));
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::SUBTITLE_TYPE, new CallableWorkerFactory(function () use ($app) {
return new SubtitleWorker($app['repo.worker-job'], $app['conf'], new LazyLocator($app, 'phraseanet.appbox'), $app['alchemy_worker.logger']);
}));
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::MAIN_QUEUE_TYPE, new CallableWorkerFactory(function () use ($app) {
return new MainQueueWorker($app['alchemy_worker.message.publisher'], $app['repo.worker-job']);
}));
} }
/** /**

View File

@@ -23,6 +23,7 @@ use Alchemy\Phrasea\WorkerManager\Subscriber\AssetsIngestSubscriber;
use Alchemy\Phrasea\WorkerManager\Subscriber\ExportSubscriber; use Alchemy\Phrasea\WorkerManager\Subscriber\ExportSubscriber;
use Alchemy\Phrasea\WorkerManager\Subscriber\RecordSubscriber; use Alchemy\Phrasea\WorkerManager\Subscriber\RecordSubscriber;
use Alchemy\Phrasea\WorkerManager\Subscriber\SearchengineSubscriber; use Alchemy\Phrasea\WorkerManager\Subscriber\SearchengineSubscriber;
use Alchemy\Phrasea\WorkerManager\Subscriber\SubtitleSubscriber;
use Alchemy\Phrasea\WorkerManager\Subscriber\WebhookSubscriber; use Alchemy\Phrasea\WorkerManager\Subscriber\WebhookSubscriber;
use Silex\Application; use Silex\Application;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -68,6 +69,7 @@ class QueueWorkerServiceProvider implements PluginProviderInterface
$dispatcher->addSubscriber(new AssetsIngestSubscriber($app['alchemy_worker.message.publisher'])); $dispatcher->addSubscriber(new AssetsIngestSubscriber($app['alchemy_worker.message.publisher']));
$dispatcher->addSubscriber(new SearchengineSubscriber($app['alchemy_worker.message.publisher'])); $dispatcher->addSubscriber(new SearchengineSubscriber($app['alchemy_worker.message.publisher']));
$dispatcher->addSubscriber(new WebhookSubscriber($app['alchemy_worker.message.publisher'])); $dispatcher->addSubscriber(new WebhookSubscriber($app['alchemy_worker.message.publisher']));
$dispatcher->addSubscriber(new SubtitleSubscriber(new LazyLocator($app, 'repo.worker-job'), $app['alchemy_worker.message.publisher']));
return $dispatcher; return $dispatcher;
}) })

View File

@@ -29,7 +29,9 @@ class AMQPConnection
MessagePublisher::CREATE_RECORD_TYPE => MessagePublisher::CREATE_RECORD_QUEUE, MessagePublisher::CREATE_RECORD_TYPE => MessagePublisher::CREATE_RECORD_QUEUE,
MessagePublisher::PULL_QUEUE => MessagePublisher::PULL_QUEUE, MessagePublisher::PULL_QUEUE => MessagePublisher::PULL_QUEUE,
MessagePublisher::POPULATE_INDEX_TYPE => MessagePublisher::POPULATE_INDEX_QUEUE, MessagePublisher::POPULATE_INDEX_TYPE => MessagePublisher::POPULATE_INDEX_QUEUE,
MessagePublisher::DELETE_RECORD_TYPE => MessagePublisher::DELETE_RECORD_QUEUE MessagePublisher::DELETE_RECORD_TYPE => MessagePublisher::DELETE_RECORD_QUEUE,
MessagePublisher::MAIN_QUEUE_TYPE => MessagePublisher::MAIN_QUEUE,
MessagePublisher::SUBTITLE_TYPE => MessagePublisher::SUBTITLE_QUEUE
]; ];
// the corresponding worker queues and retry queues, loop queue // the corresponding worker queues and retry queues, loop queue
@@ -44,7 +46,6 @@ class AMQPConnection
MessagePublisher::PULL_QUEUE => MessagePublisher::LOOP_PULL_QUEUE MessagePublisher::PULL_QUEUE => MessagePublisher::LOOP_PULL_QUEUE
]; ];
// default message TTL in retry queue in millisecond
public static $defaultFailedQueues = [ public static $defaultFailedQueues = [
MessagePublisher::WRITE_METADATAS_TYPE => MessagePublisher::FAILED_METADATAS_QUEUE, MessagePublisher::WRITE_METADATAS_TYPE => MessagePublisher::FAILED_METADATAS_QUEUE,
MessagePublisher::SUBDEF_CREATION_TYPE => MessagePublisher::FAILED_SUBDEF_QUEUE, MessagePublisher::SUBDEF_CREATION_TYPE => MessagePublisher::FAILED_SUBDEF_QUEUE,

View File

@@ -18,6 +18,12 @@ class MessagePublisher
const WEBHOOK_TYPE = 'webhook'; const WEBHOOK_TYPE = 'webhook';
const POPULATE_INDEX_TYPE = 'populateIndex'; const POPULATE_INDEX_TYPE = 'populateIndex';
const PULL_ASSETS_TYPE = 'pullAssets'; const PULL_ASSETS_TYPE = 'pullAssets';
const SUBTITLE_TYPE = 'subtitle';
const MAIN_QUEUE_TYPE = 'mainQueue';
const MAIN_QUEUE = 'main-queue';
const SUBTITLE_QUEUE = 'subtitle-queue';
// worker queue to be consumed, when no ack , it is requeued to the retry queue // worker queue to be consumed, when no ack , it is requeued to the retry queue
const EXPORT_QUEUE = 'export-queue'; const EXPORT_QUEUE = 'export-queue';

View File

@@ -0,0 +1,88 @@
<?php
namespace Alchemy\Phrasea\WorkerManager\Subscriber;
use Alchemy\Phrasea\Core\Event\Record\RecordAutoSubtitleEvent;
use Alchemy\Phrasea\Core\PhraseaEvents;
use Alchemy\Phrasea\Model\Entities\WorkerJob;
use Alchemy\Phrasea\Model\Repositories\WorkerJobRepository;
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SubtitleSubscriber implements EventSubscriberInterface
{
private $messagePublisher;
/** @var WorkerJobRepository $repoWorkerJob*/
private $repoWorkerJob;
/** @var callable */
private $repoWorkerJobLocator;
public function __construct(callable $repoWorkerJobLocator, MessagePublisher $messagePublisher)
{
$this->repoWorkerJobLocator = $repoWorkerJobLocator;
$this->messagePublisher = $messagePublisher;
}
public function onRecordAutoSubtitle(RecordAutoSubtitleEvent $event)
{
$this->repoWorkerJob = $this->getRepoWorkerJob();
$em = $this->repoWorkerJob->getEntityManager();
$data = [
"databoxId" => $event->getRecord()->getDataboxId(),
"recordId" => $event->getRecord()->getRecordId(),
"permalinkUrl" => $event->getPermalinkUrl(),
"langageSource" => $event->getLanguageSource(),
"metaStructureId" => $event->getMetaStructId()
];
$this->repoWorkerJob->reconnect();
$em->beginTransaction();
try {
$workerJob = new WorkerJob();
$workerJob
->setType(MessagePublisher::SUBTITLE_TYPE)
->setData($data)
->setStatus(WorkerJob::WAITING)
;
$em->persist($workerJob);
$em->flush();
$em->commit();
$data['workerId'] = $workerJob->getId();
$data['type'] = MessagePublisher::SUBTITLE_TYPE;
$payload = [
'message_type' => MessagePublisher::MAIN_QUEUE_TYPE,
'payload' => $data
];
$this->messagePublisher->publishMessage($payload, MessagePublisher::MAIN_QUEUE);
} catch (\Exception $e) {
$em->rollback();
}
}
public static function getSubscribedEvents()
{
return [
PhraseaEvents::RECORD_AUTO_SUBTITLE => 'onRecordAutoSubtitle',
];
}
/**
* @return WorkerJobRepository
*/
private function getRepoWorkerJob()
{
$callable = $this->repoWorkerJobLocator;
return $callable();
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Alchemy\Phrasea\WorkerManager\Worker;
use Alchemy\Phrasea\Model\Repositories\WorkerJobRepository;
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
class MainQueueWorker implements WorkerInterface
{
private $messagePublisher;
private $repoWorkerJob;
public function __construct(
MessagePublisher $messagePublisher,
WorkerJobRepository $repoWorkerJob
)
{
$this->messagePublisher = $messagePublisher;
$this->repoWorkerJob = $repoWorkerJob;
}
public function process(array $payload)
{
// if needed do treatement here depending on the type
$queue = null;
$messageType = '';
switch ($payload['type']) {
case MessagePublisher::SUBTITLE_TYPE:
$queue = MessagePublisher::SUBTITLE_QUEUE;
$messageType = $payload['type'];
unset($payload['type']);
break;
}
$data = [
'message_type' => $messageType,
'payload' => $payload
];
if ($queue != null) {
$this->messagePublisher->publishMessage($data, $queue);
}
}
}

View File

@@ -0,0 +1,212 @@
<?php
namespace Alchemy\Phrasea\WorkerManager\Worker;
use Alchemy\Phrasea\Core\Configuration\PropertyAccess;
use Alchemy\Phrasea\Model\Entities\WorkerJob;
use Alchemy\Phrasea\Model\Repositories\WorkerJobRepository;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
class SubtitleWorker implements WorkerInterface
{
/**
* @var callable
*/
private $appboxLocator;
private $logger;
private $conf;
/** @var WorkerJobRepository $repoWorkerJob*/
private $repoWorkerJob;
public function __construct(WorkerJobRepository $repoWorkerJob, PropertyAccess $conf, callable $appboxLocator, LoggerInterface $logger)
{
$this->repoWorkerJob = $repoWorkerJob;
$this->conf = $conf;
$this->appboxLocator = $appboxLocator;
$this->logger = $logger;
}
public function process(array $payload)
{
$gingaBaseurl = $this->conf->get(['externalservice', 'ginger', 'AutoSubtitling', 'service_base_url']);
$gingaToken = $this->conf->get(['externalservice', 'ginger', 'AutoSubtitling', 'token']);
$gingaTranscriptFormat = $this->conf->get(['externalservice', 'ginger', 'AutoSubtitling', 'transcript_format']);
if (!$gingaBaseurl || !$gingaToken || !$gingaTranscriptFormat) {
$this->logger->error("External service Ginga not set correctly in configuration.yml");
return 0;
}
/** @var WorkerJob $workerJob */
$workerJob = $this->repoWorkerJob->find($payload['workerId']);
if ($workerJob == null) {
$this->logger->error("WorkerId not found");
return 0;
}
$workerJob->setStatus(WorkerJob::RUNNING)
->setStarted(new \DateTime('now'));
$em = $this->repoWorkerJob->getEntityManager();
$this->repoWorkerJob->reconnect();
$em->persist($workerJob);
$em->flush();
$record = $this->getApplicationBox()->get_databox($payload['databoxId'])->get_record($payload['recordId']);
if ($payload['permalinkUrl'] != '' && $payload['metaStructureId']) {
switch ($payload['langageSource']) {
case 'En':
$language = 'en-GB';
break;
case 'De':
$language = 'de-DE';
break;
case 'Fr':
default:
$language = 'fr-FR';
break;
}
$gingerClient = new Client();
try {
$response = $gingerClient->post($gingaBaseurl.'/media/', [
'headers' => [
'Authorization' => 'token '.$gingaToken
],
'json' => [
'url' => $payload['permalinkUrl'],
'language' => $language
]
]);
} catch(\Exception $e) {
$this->logger->error($e->getMessage());
$this->jobFinished($workerJob);
return 0;
}
if ($response->getStatusCode() !== 201) {
$this->logger->error("response status /media/ : ". $response->getStatusCode());
$this->jobFinished($workerJob);
return 0;
}
$responseMediaBody = $response->getBody()->getContents();
$responseMediaBody = json_decode($responseMediaBody,true);
$checkStatus = true;
do {
// first wait 5 second before check subtitling status
sleep(5);
$this->logger->info("bigin to check status");
try {
$response = $gingerClient->get($gingaBaseurl.'/task/'.$responseMediaBody['task_id'].'/', [
'headers' => [
'Authorization' => 'token '.$gingaToken
]
]);
} catch (\Exception $e) {
$checkStatus = false;
break;
}
if ($response->getStatusCode() !== 200) {
$checkStatus = false;
break;
}
$responseTaskBody = $response->getBody()->getContents();
$responseTaskBody = json_decode($responseTaskBody,true);
} while($responseTaskBody['status'] != 'SUCCESS');
if (!$checkStatus) {
$this->logger->error("can't check status");
$this->jobFinished($workerJob);
return 0;
}
try {
$response = $gingerClient->get($gingaBaseurl.'/media/'.$responseMediaBody['media']['uuid'].'/', [
'headers' => [
'Authorization' => 'token '.$gingaToken,
'ACCEPT' => $gingaTranscriptFormat
],
'query' => [
'language' => $language
]
]);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
$this->jobFinished($workerJob);
return 0;
}
if ($response->getStatusCode() !== 200) {
$this->logger->error("response status /media/uuid : ". $response->getStatusCode());
$this->jobFinished($workerJob);
return 0;
}
$transcriptContent = $response->getBody()->getContents();
$transcriptContent = preg_replace('/WEBVTT/', 'WEBVTT - with cue identifier', $transcriptContent, 1);
$metadatas[0] = [
'meta_struct_id' => (int)$payload['metaStructureId'],
'meta_id' => '',
'value' => $transcriptContent
];
try {
$record->set_metadatas($metadatas);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
$this->jobFinished($workerJob);
return 0;
}
$this->logger->info("Auto subtitle SUCCESS");
}
$this->jobFinished($workerJob);
return 0;
}
/**
* @return \appbox
*/
private function getApplicationBox()
{
$callable = $this->appboxLocator;
return $callable();
}
private function jobFinished(WorkerJob $workerJob)
{
$workerJob->setStatus(WorkerJob::FINISHED)
->setFinished(new \DateTime('now'));
$em = $this->repoWorkerJob->getEntityManager();
$this->repoWorkerJob->reconnect();
$em->persist($workerJob);
$em->flush();
}
}

View File

@@ -319,5 +319,15 @@ workers:
user: '' user: ''
password: '' password: ''
vhost: / vhost: /
externalservice:
ginger:
AutoSubtitling:
service_base_url: https://base.uri
token: 39c6011d
transcript_format: text/vtt
subdef_source: preview
Console_logger_enabled_environments: [test] Console_logger_enabled_environments: [test]

View File

@@ -244,9 +244,9 @@
<p class="item"> <p class="item">
<label>{{ "prod:videoeditor:subtitleRequestTab:label:: Source Audio language" | trans }}</label> <label>{{ "prod:videoeditor:subtitleRequestTab:label:: Source Audio language" | trans }}</label>
<select name="subtitle_language_source" id="subtitle_language_source"> <select name="subtitle_language_source" id="subtitle_language_source">
<option value="fr">Fr</option> {% for videoTextTrackField in videoTextTrackFields %}
<option value="en">En</option> <option value="{{ videoTextTrackField.meta_struct_id}}">{{ videoTextTrackField.label}}</option>
<option value="de">De</option> {% endfor %}
</select> </select>
</p> </p>
<p class="item"> <p class="item">
@@ -354,4 +354,24 @@
overlapChapters: {% if overlapChapters != NULL %}{{ overlapChapters }}{% else %}1{% endif %}, overlapChapters: {% if overlapChapters != NULL %}{{ overlapChapters }}{% else %}1{% endif %},
} }
}; };
$('#submit-subtitle-request').on('click', function (e) {
e.preventDefault();
console.log("auto-subtitle process");
$.ajax({
type: 'POST',
url: '/prod/tools/auto-subtitle/',
dataType: 'json',
data: {
databox_id: {{ record.getDataboxId }},
record_id: {{ record.getRecordId }},
meta_struct_id: $('#subtitle_language_source').val(),
subtitle_language_source: $('#subtitle_language_source option:selected').text()
},
success: function success(data) {
console.log(data);
}
});
});
</script> </script>