View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.transcription.workflowoperation;
23  
24  import org.opencastproject.composer.api.ComposerService;
25  import org.opencastproject.composer.api.EncoderException;
26  import org.opencastproject.composer.api.EncodingProfile;
27  import org.opencastproject.job.api.Job;
28  import org.opencastproject.job.api.JobContext;
29  import org.opencastproject.mediapackage.MediaPackage;
30  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
31  import org.opencastproject.mediapackage.MediaPackageElementParser;
32  import org.opencastproject.mediapackage.MediaPackageException;
33  import org.opencastproject.mediapackage.Track;
34  import org.opencastproject.mediapackage.selector.AbstractMediaPackageElementSelector;
35  import org.opencastproject.mediapackage.selector.SimpleElementSelector;
36  import org.opencastproject.mediapackage.selector.TrackSelector;
37  import org.opencastproject.serviceregistry.api.ServiceRegistry;
38  import org.opencastproject.transcription.api.TranscriptionService;
39  import org.opencastproject.transcription.api.TranscriptionServiceException;
40  import org.opencastproject.util.NotFoundException;
41  import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
42  import org.opencastproject.workflow.api.ConfiguredTagsAndFlavors;
43  import org.opencastproject.workflow.api.WorkflowInstance;
44  import org.opencastproject.workflow.api.WorkflowOperationException;
45  import org.opencastproject.workflow.api.WorkflowOperationHandler;
46  import org.opencastproject.workflow.api.WorkflowOperationInstance;
47  import org.opencastproject.workflow.api.WorkflowOperationResult;
48  import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
49  import org.opencastproject.workspace.api.Workspace;
50  
51  import org.apache.commons.lang3.StringUtils;
52  import org.osgi.service.component.ComponentContext;
53  import org.osgi.service.component.annotations.Activate;
54  import org.osgi.service.component.annotations.Component;
55  import org.osgi.service.component.annotations.Reference;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  import java.io.IOException;
60  import java.util.Collection;
61  import java.util.List;
62  import java.util.SortedMap;
63  import java.util.TreeMap;
64  
65  @Component(
66      immediate = true,
67      service = WorkflowOperationHandler.class,
68      property = {
69          "service.description=Start Transcription Workflow Operation Handler (Microsoft Azure)",
70          "workflow.operation=microsoft-azure-start-transcription"
71      }
72  )
73  public class MicrosoftAzureStartTranscriptionOperationHandler extends AbstractWorkflowOperationHandler {
74  
75    private static final Logger logger = LoggerFactory.getLogger(MicrosoftAzureStartTranscriptionOperationHandler.class);
76  
77    /** Workflow configuration option keys */
78    static final String LANGUAGE_KEY = "language";
79    static final String SKIP_IF_FLAVOR_EXISTS_KEY = "skip-if-flavor-exists";
80    static final String EXTRACT_AUDIO_ENCODING_PROFILE_KEY = "audio-extraction-encoding-profile";
81    private static final String DEFAULT_EXTRACT_AUDIO_ENCODING_PROFILE = "transcription-azure.audio";
82  
83    /** The transcription service */
84    private TranscriptionService service = null;
85    /** The composer service. */
86    private ComposerService composerService;
87    /** The workspace. */
88    private Workspace workspace;
89  
90    /** The configuration options for this handler */
91    private static final SortedMap<String, String> CONFIG_OPTIONS;
92  
93    static {
94      CONFIG_OPTIONS = new TreeMap<String, String>();
95      CONFIG_OPTIONS.put(SOURCE_FLAVORS, "The flavors of the tracks to use as audio input. "
96          + "Only the first available track will be used.");
97      CONFIG_OPTIONS.put(SOURCE_TAGS, "The tags of the track to use as audio input.");
98      CONFIG_OPTIONS.put(LANGUAGE_KEY, "The language the transcription service should use.");
99      CONFIG_OPTIONS.put(SKIP_IF_FLAVOR_EXISTS_KEY,
100         "If this \"flavor\" is already in the media package, skip this operation.");
101     CONFIG_OPTIONS.put(EXTRACT_AUDIO_ENCODING_PROFILE_KEY,
102         "The encoding profile to extract audio for transcription.");
103   }
104 
105   @Override
106   @Activate
107   protected void activate(ComponentContext cc) {
108     super.activate(cc);
109   }
110 
111   @Override
112   public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
113           throws WorkflowOperationException {
114     MediaPackage mediaPackage = workflowInstance.getMediaPackage();
115     WorkflowOperationInstance operation = workflowInstance.getCurrentOperation();
116 
117     String skipOption = StringUtils.trimToNull(operation.getConfiguration(SKIP_IF_FLAVOR_EXISTS_KEY));
118     if (skipOption != null) {
119       SimpleElementSelector elementSelector = new SimpleElementSelector();
120       for (String flavorStr : StringUtils.split(skipOption, ",")) {
121         if (StringUtils.trimToNull(flavorStr) == null) {
122           continue;
123         }
124         MediaPackageElementFlavor skipFlavor = MediaPackageElementFlavor.parseFlavor(
125             StringUtils.trimToEmpty(flavorStr));
126         elementSelector.addFlavor(skipFlavor);
127       }
128       if (!elementSelector.select(mediaPackage, false).isEmpty()) {
129         logger.info("Start transcription operation will be skipped for media package '{}' "
130             + "because elements with given flavor already exist.", mediaPackage.getIdentifier());
131         return createResult(Action.SKIP);
132       }
133     }
134     String encodingProfile = StringUtils.trimToNull(operation.getConfiguration(EXTRACT_AUDIO_ENCODING_PROFILE_KEY));
135     if (encodingProfile == null) {
136       encodingProfile = DEFAULT_EXTRACT_AUDIO_ENCODING_PROFILE;
137     }
138     // Check which tags have been configured
139     ConfiguredTagsAndFlavors tagsAndFlavors = getTagsAndFlavors(
140         workflowInstance, Configuration.many, Configuration.many, Configuration.none, Configuration.none);
141     List<MediaPackageElementFlavor> sourceFlavorsOption = tagsAndFlavors.getSrcFlavors();
142     List<String> sourceTagsOption = tagsAndFlavors.getSrcTags();
143     String language = StringUtils.trimToEmpty(operation.getConfiguration(LANGUAGE_KEY));
144     AbstractMediaPackageElementSelector<Track> elementSelector = new TrackSelector();
145     // Make sure either one of tags or flavors are provided
146     if (sourceTagsOption.isEmpty() && sourceFlavorsOption.isEmpty()) {
147       throw new WorkflowOperationException("No source tag or flavor have been specified!");
148     }
149     if (!sourceFlavorsOption.isEmpty()) {
150       for (MediaPackageElementFlavor srcFlavor : sourceFlavorsOption) {
151         elementSelector.addFlavor(srcFlavor);
152       }
153     }
154     if (!sourceTagsOption.isEmpty()) {
155       for (String srcTag : sourceTagsOption) {
156         elementSelector.addTag(StringUtils.trimToEmpty(srcTag));
157       }
158     }
159     Collection<Track> elements = elementSelector.select(mediaPackage, false);
160     if (elements.isEmpty()) {
161       logger.info("Media package {} does not contain elements to transcribe. Skip operation.",
162           mediaPackage.getIdentifier());
163       return createResult(Action.SKIP);
164     }
165     logger.info("Start transcription for media package '{}'.", mediaPackage.getIdentifier());
166     Track audioTrack = null;
167     try {
168       for (Track track : elements) {
169         if (!track.hasAudio()) {
170           continue;
171         }
172         try {
173           EncodingProfile profile = composerService.getProfile(encodingProfile);
174           if (profile == null) {
175             throw new WorkflowOperationException("Encoding profile '" + encodingProfile + "' was not found.");
176           }
177           Job encodeJob = composerService.encode(track, encodingProfile);
178           if (!waitForStatus(encodeJob).isSuccess()) {
179             throw new WorkflowOperationException(String.format(
180                 "Audio extraction job for track %s did not complete successfully.", track.getURI()));
181           }
182           audioTrack = (Track) MediaPackageElementParser.getFromXml(encodeJob.getPayload());
183         } catch (EncoderException | MediaPackageException e) {
184           throw new WorkflowOperationException(
185               String.format("Extracting audio for transcription failed for the track %s", track.getURI()), e);
186         }
187         try {
188           Job transcriptionJob = service.startTranscription(mediaPackage.getIdentifier().toString(), audioTrack,
189               language);
190           // Wait for the jobs to return
191           if (!waitForStatus(transcriptionJob).isSuccess()) {
192             throw new WorkflowOperationException("Transcription job did not complete successfully.");
193           }
194           // Return OK means that the transcription job was created, but not finished yet
195           logger.debug("External transcription job for media package '{}' was created.", mediaPackage.getIdentifier());
196           // Only one job per media package
197           // Results are empty, we should get a callback when transcription is done
198           return createResult(Action.CONTINUE);
199         } catch (TranscriptionServiceException e) {
200           throw new WorkflowOperationException(e);
201         }
202       }
203     } finally {
204       // We do not need the audio file anymore, delete it...
205       deleteTrack(audioTrack);
206     }
207     // If we are here, no audio tracks are found. Skip operation.
208     logger.info("Media package {} does not contain audio stream to transcribe. Skip operation.",
209         mediaPackage.getIdentifier());
210     return createResult(Action.SKIP);
211   }
212 
213   protected void deleteTrack(Track track) {
214     if (track != null && track.getURI() != null) {
215       try {
216         workspace.delete(track.getURI());
217       } catch (NotFoundException ex) {
218         // do nothing
219       } catch (IOException ex) {
220         logger.warn("Unable to delete file {}", track.getURI());
221       }
222     }
223   }
224 
225   @Reference(target = "(provider=microsoft.azure)")
226   public void setTranscriptionService(TranscriptionService service) {
227     this.service = service;
228   }
229 
230   @Reference
231   @Override
232   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
233     super.setServiceRegistry(serviceRegistry);
234   }
235 
236   @Reference
237   public void setComposerService(ComposerService composerService) {
238     this.composerService = composerService;
239   }
240 
241   @Reference
242   public void setWorkspace(Workspace workspace) {
243     this.workspace = workspace;
244   }
245 }