@@ -18,6 +18,7 @@ import com.microsoft.cognitiveservices.speech.transcription.{Conversation, Conve
1818 ConversationTranscriptionEventArgs , Participant }
1919import com .microsoft .cognitiveservices .speech .util .EventHandler
2020import org .apache .commons .io .FilenameUtils
21+ import org .apache .commons .io .input .TeeInputStream
2122import org .apache .hadoop .fs .Path
2223import org .apache .spark .broadcast .Broadcast
2324import org .apache .spark .injections .SConf
@@ -30,19 +31,64 @@ import org.apache.spark.sql.types._
3031import org .apache .spark .sql .{DataFrame , Dataset , Row }
3132import spray .json ._
3233
33- import java .io .{BufferedInputStream , ByteArrayInputStream , Closeable , InputStream }
34+ import java .io .{BufferedInputStream , ByteArrayInputStream , Closeable , FileOutputStream , IOException , InputStream }
3435import java .lang .ProcessBuilder .Redirect
3536import java .net .{URI , URL }
36- import java .util .UUID
37+ import java .util .{ Locale , UUID }
3738import java .util .concurrent .{LinkedBlockingQueue , TimeUnit }
3839import scala .concurrent .{ExecutionContext , Future , blocking }
3940import scala .language .existentials
41+ import scala .util .Try
4042
4143object SpeechToTextSDK extends ComplexParamsReadable [SpeechToTextSDK ]
4244
45+ private [speech] object SpeechSDKBase {
46+ private val FfmpegOutputArgs = Seq (" -acodec" , " mp3" , " -ab" , " 257k" , " -f" , " mp3" )
47+ private val FfmpegProtocolWhitelist = " http,https,tcp,tls,crypto"
48+ private val HttpSchemes = Set (" http" , " https" )
49+ private val UriSchemePattern = " ^[A-Za-z][A-Za-z0-9+.-]*:.*" .r
50+ private val WindowsDrivePathPattern = " ^[A-Za-z]:[\\\\ /].*" .r
51+
52+ def parseUri (uri : String ): Option [URI ] = Try (new URI (uri)).toOption
53+
54+ def isHttpUri (uri : URI ): Boolean =
55+ Option (uri.getScheme).map(_.toLowerCase(Locale .ROOT )).exists(HttpSchemes )
56+
57+ def validateFfmpegUri (uri : String ): URI = {
58+ val parsedUri = parseUri(uri).getOrElse {
59+ throw new IllegalArgumentException (" ffmpeg input URI must be a valid http(s) URI" )
60+ }
61+ require(isHttpUri(parsedUri), " ffmpeg input URI must use the http or https scheme" )
62+ parsedUri
63+ }
64+
65+ def validateRecordedFileName (fileName : String ): String = {
66+ val fn = Option (fileName).filter(_.trim.nonEmpty).getOrElse {
67+ throw new IllegalArgumentException (" Recorded file name must be non-empty when recordAudioData is true" )
68+ }
69+ val hasUriScheme = UriSchemePattern .pattern.matcher(fn).matches()
70+ val isWindowsDrivePath = WindowsDrivePathPattern .pattern.matcher(fn).matches()
71+
72+ require(! fn.startsWith(" -" ), " Recorded file name must not start with '-'" )
73+ require(! fn.contains('\u0000 ' ), " Recorded file name must not contain NUL characters" )
74+ require(! hasUriScheme || (OsUtils .IsWindows && isWindowsDrivePath),
75+ " Recorded file name must be a local file path without a URI scheme" )
76+ fn
77+ }
78+
79+ def makeFfmpegCommand (uri : String ,
80+ extraArgs : Seq [String ]): Seq [String ] = {
81+ validateFfmpegUri(uri)
82+ val outputArgs = extraArgs ++ FfmpegOutputArgs
83+ Seq (" ffmpeg" , " -y" ,
84+ " -reconnect" , " 1" , " -reconnect_streamed" , " 1" , " -reconnect_delay_max" , " 2000" ,
85+ " -protocol_whitelist" , FfmpegProtocolWhitelist , " -i" , uri) ++ outputArgs ++ Seq (" pipe:1" )
86+ }
87+ }
88+
4389// scalastyle:off no.finalize
4490private [ml] class BlockingQueueIterator [T ](lbq : LinkedBlockingQueue [Option [T ]],
45- onClose : => Unit ) extends Iterator [T ] with Closeable {
91+ onClose : => Unit ) extends Iterator [T ] with Closeable {
4692 var nextVar : Option [T ] = None
4793 var isDone = false
4894 var takeAnother = true
@@ -242,31 +288,38 @@ abstract class SpeechSDKBase extends Transformer
242288 dynamicParamRow : Row ): (InputStream , String ) = {
243289 if (isUriAudio) { // scalastyle:ignore cyclomatic.complexity
244290 val uri = row.getAs[String ](getAudioDataCol)
245- val ffmpegCommand : Seq [ String ] = {
246- val body = Seq ( " ffmpeg " , " -y " ,
247- " -reconnect " , " 1 " , " -reconnect_streamed " , " 1 " , " -reconnect_delay_max " , " 2000 " ,
248- " -i " , uri) ++ getExtraFfmpegArgs ++ Seq ( " -acodec " , " mp3 " , " -ab " , " 257k " , " -f " , " mp3 " , " pipe:1 " )
249-
250- if (getRecordAudioData && OsUtils . IsWindows ) {
251- val fn = row.getAs[ String ](getRecordedFileNameCol )
252- body ++ Seq ( " -acodec " , " mp3 " , " -ab " , " 257k " , " -f " , " mp3 " , fn)
253- } else if (getRecordAudioData && ! OsUtils . IsWindows ) {
254- val fn = row.getAs[ String ](getRecordedFileNameCol)
255- Seq ( " /bin/sh " , " -c " , (body ++ Seq ( " | " , " tee " , fn)).mkString( " " ))
291+ val parsedUriOpt = SpeechSDKBase .parseUri(uri)
292+ val extension = parsedUriOpt
293+ .flatMap(parsedUri => Option (parsedUri.getPath))
294+ .map( FilenameUtils .getExtension )
295+ .getOrElse( FilenameUtils .getExtension(uri))
296+ .toLowerCase( Locale . ROOT )
297+ val isHttpUri = parsedUriOpt.exists( SpeechSDKBase .isHttpUri )
298+
299+ if (Set ( " m3u8 " , " m4a " )(extension) && isHttpUri ) {
300+ val recordedFileName = if (getRecordAudioData) {
301+ Some ( SpeechSDKBase .validateRecordedFileName(row.getAs[ String ](getRecordedFileNameCol) ))
256302 } else {
257- body
303+ None
258304 }
259- }
260-
261- val extension = FilenameUtils .getExtension(new URI (uri).getPath).toLowerCase()
262-
263- if (Set (" m3u8" , " m4a" )(extension) && uri.startsWith(" http" )) {
305+ val ffmpegCommand = SpeechSDKBase .makeFfmpegCommand(uri, getExtraFfmpegArgs.toSeq)
264306 val proc = new ProcessBuilder ()
265307 .redirectError(Redirect .INHERIT )
266308 .redirectInput(Redirect .INHERIT )
267309 .command(ffmpegCommand : _* )
268310 .start()
269- val stream = proc.getInputStream
311+ val stream = recordedFileName match {
312+ case Some (fn) =>
313+ try {
314+ new TeeInputStream (proc.getInputStream, new FileOutputStream (fn), true )
315+ } catch {
316+ case e : IOException =>
317+ proc.destroy()
318+ throw e
319+ }
320+ case None =>
321+ proc.getInputStream
322+ }
270323
271324 if (getExtraFfmpegArgs.contains(" -t" )) {
272325 val timeLimit = getExtraFfmpegArgs(getExtraFfmpegArgs.indexOf(" -t" ) + 1 ).toInt
@@ -285,7 +338,7 @@ abstract class SpeechSDKBase extends Transformer
285338 }
286339
287340 (stream, " mp3" )
288- } else if (uri.startsWith( " http " ) ) {
341+ } else if (isHttpUri ) {
289342 val conn = new URL (uri).openConnection
290343 conn.setConnectTimeout(5000 ) // scalastyle:ignore magic.number
291344 conn.setReadTimeout(5000 ) // scalastyle:ignore magic.number
0 commit comments