diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ArgumentAttribute.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ArgumentAttribute.cs
new file mode 100644
index 00000000..af86215b
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ArgumentAttribute.cs
@@ -0,0 +1,174 @@
+namespace Microsoft.SpeechServices.CommonLib.CommandParser;
+
+using System;
+using System.Globalization;
+
+[AttributeUsage(AttributeTargets.Field, Inherited = false)]
+public sealed class ArgumentAttribute : Attribute
+{
+ private readonly string flag;
+ private string description = string.Empty;
+ private string usagePlaceholder;
+ private bool optional;
+ private bool hidden;
+ private InOutType inoutType;
+ private string requiredModes;
+ private string optionalModes;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Flag string for this attribute.
+ public ArgumentAttribute(string optionName)
+ {
+ if (optionName == null)
+ {
+ throw new ArgumentNullException(nameof(optionName));
+ }
+
+ this.flag = optionName;
+ }
+
+ ///
+ /// Gets The parse recognising flag.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Ignore.")]
+ public string OptionName
+ {
+ get { return this.flag.ToLower(CultureInfo.InvariantCulture); }
+ }
+
+ ///
+ /// Gets or sets Description will display in the PrintUsage method.
+ ///
+ public string Description
+ {
+ get
+ {
+ return this.description;
+ }
+
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ this.description = value;
+ }
+ }
+
+ ///
+ /// Gets or sets In the PrintUsage method this will display a place hold for a parameter.
+ ///
+ public string UsagePlaceholder
+ {
+ get
+ {
+ return this.usagePlaceholder;
+ }
+
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ this.usagePlaceholder = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether (optional = true) means not necessarily in the command-line.
+ ///
+ public bool Optional
+ {
+ get { return this.optional; }
+ set { this.optional = value; }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether (Hidden = true) means this option will not be printed in the command-line.
+ /// While one option is set with Hidden, the Optional must be true.
+ ///
+ public bool Hidden
+ {
+ get { return this.hidden; }
+ set { this.hidden = value; }
+ }
+
+ ///
+ /// Gets or sets The in/out type of argument.
+ ///
+ public InOutType InOutType
+ {
+ get { return this.inoutType; }
+ set { this.inoutType = value; }
+ }
+
+ ///
+ /// Gets or sets The modes require this argument.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Ignore.")]
+ public string RequiredModes
+ {
+ get
+ {
+ return this.requiredModes;
+ }
+
+ set
+ {
+ this.requiredModes = value?.ToLower(CultureInfo.InvariantCulture);
+ }
+ }
+
+ ///
+ /// Gets or sets The modes optionally require this argument.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Ignore.")]
+ public string OptionalModes
+ {
+ get
+ {
+ return this.optionalModes;
+ }
+
+ set
+ {
+ this.optionalModes = value?.ToLower(CultureInfo.InvariantCulture);
+ }
+ }
+
+ ///
+ /// Get required modes in an array.
+ ///
+ /// Mode array.
+ public string[] GetRequiredModeArray()
+ {
+ string[] modes = null;
+ if (!string.IsNullOrEmpty(this.requiredModes))
+ {
+ modes = this.requiredModes.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ return modes;
+ }
+
+ ///
+ /// Get optional modes in an array.
+ ///
+ /// Mode array.
+ public string[] GetOptionalModeArray()
+ {
+ string[] modes = null;
+ if (!string.IsNullOrEmpty(this.optionalModes))
+ {
+ modes = this.optionalModes.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ return modes;
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/CommandLineParser.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/CommandLineParser.cs
new file mode 100644
index 00000000..4a279562
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/CommandLineParser.cs
@@ -0,0 +1,1282 @@
+namespace Microsoft.SpeechServices.CommonLib.CommandParser;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Security.Permissions;
+using System.Text;
+
+public static class CommandLineParser
+{
+ public static void Parse(string[] args, object target)
+ {
+ ClpHelper.CheckTarget(target);
+ ClpHelper.CheckArgs(args, target);
+
+ InternalFlags internalTarget = new InternalFlags();
+ ClpHelper helper = new ClpHelper(target, internalTarget);
+
+ helper.ParseArgs(args);
+
+ if (!string.IsNullOrEmpty(internalTarget.ConfigFile))
+ {
+ args = ClpHelper.GetStringsFromConfigFile(internalTarget.ConfigFile);
+ helper.ParseArgs(args);
+ }
+
+ if (internalTarget.NeedHelp)
+ {
+ throw new CommandLineParseException(string.Empty, "help");
+ }
+
+ helper.CheckAllRequiredDestination();
+ }
+
+ public static void PrintUsage(object target)
+ {
+ string usage = BuildUsage(target);
+ Console.WriteLine();
+ Console.WriteLine(usage);
+ }
+
+ public static void HandleException(object target, Exception exception)
+ {
+ ArgumentNullException.ThrowIfNull(exception);
+ if (!string.IsNullOrEmpty(exception.Message))
+ {
+ ArgumentNullException.ThrowIfNull(exception.Message);
+ }
+ else
+ {
+ PrintUsage(target);
+ }
+ }
+
+ public static string BuildUsage(object target)
+ {
+ ClpHelper.CheckTarget(target);
+
+ CommentAttribute[] ca = (CommentAttribute[])target.GetType().GetCustomAttributes(typeof(CommentAttribute), false);
+
+ StringBuilder sb = new StringBuilder();
+
+ if (ca.Length == 1 && !string.IsNullOrEmpty(ca[0].HeadComment))
+ {
+ sb.AppendLine(ca[0].HeadComment);
+ }
+
+ sb.AppendLine();
+
+ Assembly entryAssm = Assembly.GetEntryAssembly();
+
+ // entryAssm is a null reference when a managed assembly has been loaded
+ // from an unmanaged application; Currently we don't allow such calling.
+ // But when calling by our Nunit test framework, this value is null
+ if (entryAssm != null)
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, "Version {0}", entryAssm.GetName().Version.ToString());
+ sb.AppendLine();
+ }
+
+ sb.Append(ClpHelper.BuildUsageLine(target));
+ sb.Append(ClpHelper.BuildOptionsString(target));
+
+ if (ca.Length == 1 && !string.IsNullOrEmpty(ca[0].RearComment))
+ {
+ sb.AppendLine();
+ sb.AppendLine(ca[0].RearComment);
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Command line parser helper class.
+ ///
+ private class ClpHelper
+ {
+ public const BindingFlags AllFieldBindingFlags =
+ BindingFlags.Instance | BindingFlags.Static |
+ BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.DeclaredOnly;
+
+ public const string Mode = "mode";
+
+ // const members.
+ private const int MaxCommandLineStringNumber = 800;
+ private const int MaxConfigFileSize = 32 * 1024; // 32k
+
+ private string modeString;
+
+ // class members.
+ private object clpTarget;
+ private InternalFlags internalTarget;
+ private Dictionary destMap = new Dictionary();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Target object to reflect usage information.
+ /// Internal flags.
+ public ClpHelper(object target, InternalFlags internalTarget)
+ {
+ this.clpTarget = target;
+ this.internalTarget = internalTarget; // interal flags class, include "-h","-?","-help","-C"
+
+ this.ParseTheDestination(target);
+ }
+
+ ///
+ /// Check the target objcet, which is to save the value, to avoid misuse.
+ ///
+ /// Target object to reflect usage information.
+ public static void CheckTarget(object target)
+ {
+ if (target == null)
+ {
+ throw new ArgumentNullException(nameof(target));
+ }
+
+ if (!target.GetType().IsClass)
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Object target is not a class."), "target");
+ }
+
+ // Check each field of the target class to ensure that every field, which wanted to be
+ // filled, has defined a static TryParse(string, out Value) funtion. In the parsing time,
+ // the CLP class will use this static function to parse the string to value.
+ foreach (FieldInfo fieldInfo in target.GetType().GetFields(ClpHelper.AllFieldBindingFlags))
+ {
+ if (fieldInfo.IsDefined(typeof(ArgumentAttribute), false))
+ {
+ Type type = fieldInfo.FieldType;
+ if (type.IsArray)
+ {
+ type = type.GetElementType();
+ }
+
+ // string Type don't need a TryParse function, so skip check it.
+ if (type == typeof(string))
+ {
+ continue;
+ }
+
+ Type reftype = Type.GetType(type.ToString() + "&");
+ if (reftype == null)
+ {
+ throw new ArgumentException(
+ "This Type does not exist in this assembly GetType(" + type + ")failed.",
+ fieldInfo.ToString());
+ }
+
+ MethodInfo mi = type.GetMethod("TryParse", new Type[] { typeof(string), reftype });
+ if (mi == null)
+ {
+ throw new ArgumentException(
+ "Type " + type + " don't have a TryParse(string, out Value) method.",
+ fieldInfo.ToString());
+ }
+ }
+ }
+ }
+
+ ///
+ /// Check args from static Main() function, to avoid misuse this library.
+ ///
+ /// Argument string array.
+ /// Target object to reflect usage information.
+ public static void CheckArgs(string[] args, object target)
+ {
+ if (args == null)
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "Empty parameters.");
+ throw new CommandLineParseException(message);
+ }
+
+ int requiredArgumentCount = GetRequiredArgumentCount(target);
+ if (args.Length == 0)
+ {
+ // if there is no parameter given
+ if (requiredArgumentCount > 0)
+ {
+ // some parameters are required
+ throw new CommandLineParseException(string.Empty, "help");
+ }
+
+ // run the application with default option values
+ }
+
+ if (args.Length > MaxCommandLineStringNumber)
+ {
+ throw new CommandLineParseException(string.Format(CultureInfo.InvariantCulture, "Input parameter number is larger than {0}.", MaxCommandLineStringNumber), "args");
+ }
+
+ for (int i = 0; i < args.Length; ++i)
+ {
+ if (string.IsNullOrEmpty(args[i]))
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "The {0}(th) parameter in the command line could not be null or empty.", i + 1);
+ throw new CommandLineParseException(message);
+ }
+ }
+ }
+
+ ///
+ /// Parse the configuration file into a string[], this string[] will be send to the
+ /// ParseArgs(string[] args). This function will do some simple check of the
+ /// Command line, the first character the config line in the file must '-',
+ /// Otherwise, this line will not be parsed.
+ ///
+ /// Configuration file path.
+ /// Configuration strings.
+ public static string[] GetStringsFromConfigFile(string filePath)
+ {
+ if (!File.Exists(filePath))
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "The configuration file [{0}] can not found.", filePath);
+ throw new CommandLineParseException(message, filePath);
+ }
+
+ FileInfo fileInfo = new FileInfo(filePath);
+
+ if (fileInfo.Length > MaxConfigFileSize)
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "Not supported configuration file [{0}], for the size of it is bigger than {1} byte.", filePath, MaxConfigFileSize);
+ throw new CommandLineParseException(message, filePath);
+ }
+
+ string[] lines;
+ using (StreamReader streamFile = new StreamReader(filePath))
+ {
+ lines = streamFile.ReadToEnd().Split(Environment.NewLine.ToCharArray());
+ }
+
+ List strList = new List();
+
+ // Go through the file, and expand the listed parameters
+ // into the List of existing parameters.
+ foreach (string line in lines)
+ {
+ string trimedLine = line.Trim();
+
+ if (trimedLine.IndexOf('-') == 0)
+ {
+ string[] strArray = trimedLine.Split(new char[] { ' ', '\t' });
+ foreach (string str in strArray)
+ {
+ if (!string.IsNullOrEmpty(str))
+ {
+ strList.Add(str);
+ }
+ }
+ }
+ }
+
+ return strList.ToArray();
+ }
+
+ ///
+ /// Count the number of required arguments.
+ ///
+ /// Target object to reflect usage information.
+ /// The number of required arguments.
+ public static int GetRequiredArgumentCount(object target)
+ {
+ int count = 0;
+ foreach (FieldInfo field in target.GetType().GetFields(AllFieldBindingFlags))
+ {
+ ArgumentAttribute argument = GetFieldArgumentAttribute(field);
+ if (argument == null)
+ {
+ continue; // skip those field that don't define the ArgumentAttribute.
+ }
+
+ if (!argument.Optional)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ ///
+ /// Build the useage line. First print the file name of current execution files.
+ /// And then, print the each flag of these options.
+ ///
+ /// Target object to reflect usage information.
+ /// Useage string.
+ public static string BuildUsageLine(object target)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendFormat(CultureInfo.InvariantCulture, "Usage:{0}", Environment.NewLine);
+
+ string[] allModes = GetAllModes(target);
+ if (allModes != null)
+ {
+ foreach (string mode in allModes)
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, @" Mode ""{0}"" has following usage: {1}", mode, Environment.NewLine);
+ sb.AppendFormat(CultureInfo.InvariantCulture, " {0} -mode {1}", AppDomain.CurrentDomain.FriendlyName, mode);
+
+ foreach (FieldInfo field in target.GetType().GetFields(AllFieldBindingFlags))
+ {
+ ArgumentAttribute argument = GetFieldArgumentAttribute(field);
+ if (argument == null)
+ {
+ continue; // skip those field that don't define the ArgumentAttribute.
+ }
+
+ if (argument.OptionName == ClpHelper.Mode)
+ {
+ continue;
+ }
+
+ string[] optionalModes = argument.GetOptionalModeArray();
+ string[] requiredModes = argument.GetRequiredModeArray();
+ if (requiredModes == null && optionalModes == null)
+ {
+ // should not print out hidden argument
+ if (!argument.Hidden)
+ {
+ if (argument.Optional)
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, " [{0}]", GetFlagAndPlaceHolderString(argument));
+ }
+ else
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, " {0}", GetFlagAndPlaceHolderString(argument));
+ }
+ }
+ }
+ else
+ {
+ if ((optionalModes != null) && IsInArray(optionalModes, mode))
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, " [{0}]", GetFlagAndPlaceHolderString(argument));
+ }
+ else if (requiredModes != null && IsInArray(requiredModes, mode))
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, " {0}", GetFlagAndPlaceHolderString(argument));
+ }
+ }
+ }
+
+ sb.AppendLine(string.Empty);
+ sb.AppendLine(string.Empty);
+ }
+ }
+ else
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, " {0}", AppDomain.CurrentDomain.FriendlyName);
+
+ foreach (FieldInfo field in target.GetType().GetFields(AllFieldBindingFlags))
+ {
+ ArgumentAttribute argument = GetFieldArgumentAttribute(field);
+ if (argument == null)
+ {
+ continue; // skip those field that don't define the ArgumentAttribute.
+ }
+
+ string optionLine = BuildOptionLine(argument);
+ sb.Append(optionLine);
+ }
+ }
+
+ sb.AppendLine();
+ sb.AppendLine();
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Print flag and description of each options.
+ ///
+ /// Target object to reflect usage information.
+ /// Flag and description string of each options.
+ public static string BuildOptionsString(object target)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine(" Options\tDescriptions");
+ sb.Append(BuildOptionsString(target, null));
+ return sb.ToString();
+ }
+
+ ///
+ /// Parse the args string from the static Main() or from configuration file.
+ ///
+ /// Argument string array.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Ignore.")]
+ public void ParseArgs(string[] args)
+ {
+ Destination destination = null;
+
+ foreach (string str in args)
+ {
+ Destination dest = this.IsFlagStringAndGetTheDestination(str);
+
+ // Is a flag string
+ if (dest != null)
+ {
+ if (destination != null)
+ {
+ destination.Save(this.clpTarget);
+ }
+
+ destination = dest;
+
+ if (destination.AlreadySaved)
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "The option flag [-{0}] could not be dupalicated.", destination.Argument.OptionName);
+ throw new CommandLineParseException(message, str);
+ }
+ }
+ else
+ {
+ if (destination == null)
+ {
+ destination = this.SaveValueStringToEmptyFlag(str);
+ }
+ else
+ {
+ if (!destination.TryToAddValue(this.clpTarget, str))
+ {
+ destination.Save(this.clpTarget);
+ destination = this.SaveValueStringToEmptyFlag(str);
+ }
+ }
+
+ if (destination != null)
+ {
+ if (destination.Argument.OptionName == ClpHelper.Mode)
+ {
+ this.modeString = str.ToLower(CultureInfo.InvariantCulture);
+ }
+ }
+ }
+ }
+
+ // deal with the last flag
+ if (destination != null)
+ {
+ destination.Save(this.clpTarget);
+ }
+ }
+
+ ///
+ /// By the end of the command line parsing, we must make sure that all non-optional
+ /// Flags have been given by the tool user.
+ ///
+ public void CheckAllRequiredDestination()
+ {
+ string[] allModes = GetAllModes(this.clpTarget);
+ foreach (Destination destination in this.destMap.Values)
+ {
+ bool requiredMissing = false;
+ if (destination.InternalTarget != null)
+ {
+ continue;
+ }
+
+ if (allModes != null)
+ {
+ Debug.Assert(this.destMap.ContainsKey(Mode), "Failed");
+ if (!string.IsNullOrEmpty(this.modeString))
+ {
+ string[] requireModes = destination.Argument.GetRequiredModeArray();
+ string[] optionalModes = destination.Argument.GetOptionalModeArray();
+ if (requireModes == null)
+ {
+ if (optionalModes == null)
+ {
+ // if required modes and optional modes are all empty
+ // Means the argument is commonly optional or not in all modes.
+ // we can use the Optional flag to simplify
+ requiredMissing = !destination.Argument.Optional && !destination.AlreadySaved;
+ }
+ }
+ else
+ {
+ if (IsInArray(requireModes, this.modeString))
+ {
+ requiredMissing = !destination.AlreadySaved;
+ }
+ }
+
+ if (destination.AlreadySaved && (optionalModes != null || requireModes != null))
+ {
+ if ((requireModes == null && !IsInArray(optionalModes, this.modeString)) ||
+ (optionalModes == null && !IsInArray(requireModes, this.modeString)) ||
+ (requireModes != null && optionalModes != null &&
+ !IsInArray(optionalModes, this.modeString) && !IsInArray(requireModes, this.modeString)))
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "Parameter [{0}] is not needed for mode [{1}].", destination.Argument.OptionName, this.modeString);
+ throw new CommandLineParseException(message);
+ }
+ }
+ }
+ else
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "The mode option is required for the command.");
+ throw new CommandLineParseException(message);
+ }
+ }
+ else
+ {
+ requiredMissing = !destination.Argument.Optional && !destination.AlreadySaved;
+ }
+
+ if (requiredMissing)
+ {
+ string optionLine = BuildOptionLine(destination.Argument);
+ string message = string.Format(CultureInfo.InvariantCulture, "The option '{0}' is required for the command.", optionLine.Trim());
+ throw new CommandLineParseException(message, "-" + destination.Argument.OptionName);
+ }
+ }
+ }
+
+ ///
+ /// Check if a value is in array.
+ ///
+ /// Array.
+ /// Value.
+ /// Boolean.
+ private static bool IsInArray(string[] arr, string value)
+ {
+ bool found = false;
+ for (int i = 0; i < arr.Length; i++)
+ {
+ if (arr[i] == value)
+ {
+ found = true;
+ break;
+ }
+ }
+
+ return found;
+ }
+
+ private static void CheckModeArray(string[] totalModes, string[] modes)
+ {
+ ArgumentNullException.ThrowIfNull(totalModes);
+ if (modes == null)
+ {
+ return;
+ }
+
+ string msg = "Mode {0} should be listed in mode argument's Modes string.";
+ if (modes != null)
+ {
+ for (int i = 0; i < modes.Length; i++)
+ {
+ if (!IsInArray(totalModes, modes[i]))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, msg, modes[i]));
+ }
+ }
+ }
+ }
+
+ private static string BuildOptionsString(object target, string mode)
+ {
+ StringBuilder sb = new StringBuilder();
+ foreach (FieldInfo field in target.GetType().GetFields(AllFieldBindingFlags))
+ {
+ ArgumentAttribute argument = GetFieldArgumentAttribute(field);
+ if (argument == null)
+ {
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(mode))
+ {
+ string[] modeArray = argument.GetRequiredModeArray();
+ if (modeArray != null)
+ {
+ bool found = IsInArray(modeArray, mode);
+ if (!found)
+ {
+ continue;
+ }
+ }
+ }
+
+ if (!argument.Hidden)
+ {
+ string str = field.FieldType.ToString();
+ int i = str.LastIndexOf('.');
+ str = str.Substring(i == -1 ? 0 : i + 1);
+
+ sb.AppendFormat(CultureInfo.InvariantCulture, " {0}{1}\t\t({3}) {2}", GetFlagAndPlaceHolderString(argument), Environment.NewLine, argument.Description, str);
+ if (argument.InOutType != InOutType.Unknown)
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, " [{0}]", Enum.GetName(typeof(InOutType), argument.InOutType));
+ }
+
+ sb.Append(Environment.NewLine);
+ }
+ else
+ {
+ if (!argument.Optional)
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "Argument for {0} can be hidden but can not be optional at the meantime.", field.Name);
+ Debug.Assert(argument.Optional, message);
+ throw new ArgumentException(message);
+ }
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ private static string[] GetAllModes(object target)
+ {
+ ArgumentAttribute modeArgument = null;
+ foreach (FieldInfo field in target.GetType().GetFields(AllFieldBindingFlags))
+ {
+ ArgumentAttribute argument = GetFieldArgumentAttribute(field);
+ if (argument == null)
+ {
+ continue;
+ }
+
+ if (argument.OptionName == ClpHelper.Mode)
+ {
+ modeArgument = argument;
+ break;
+ }
+ }
+
+ if (modeArgument == null)
+ {
+ return null;
+ }
+
+ string[] modeArray = modeArgument.GetRequiredModeArray();
+ if (modeArray == null || modeArray.Length == 0)
+ {
+ return null;
+ }
+
+ foreach (FieldInfo fieldInfo in target.GetType().GetFields(AllFieldBindingFlags))
+ {
+ ArgumentAttribute argument = GetFieldArgumentAttribute(fieldInfo);
+ if (argument == null)
+ {
+ continue;
+ }
+
+ string[] requiredModes = argument.GetRequiredModeArray();
+ string[] optionalModes = argument.GetOptionalModeArray();
+ CheckModeArray(modeArray, requiredModes);
+ CheckModeArray(modeArray, optionalModes);
+ if (requiredModes != null && optionalModes != null)
+ {
+ for (int i = 0; i < requiredModes.Length; i++)
+ {
+ if (IsInArray(optionalModes, requiredModes[i]))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Required modes {0} is conflicted with optional modes {1}", argument.RequiredModes, argument.OptionalModes));
+ }
+ }
+ }
+ }
+
+ return modeArray;
+ }
+
+ private static string BuildOptionLine(ArgumentAttribute argument)
+ {
+ StringBuilder sb = new StringBuilder();
+ if (!argument.Hidden)
+ {
+ if (argument.Optional)
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, " [{0}]", GetFlagAndPlaceHolderString(argument));
+ }
+ else
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, " {0}", GetFlagAndPlaceHolderString(argument));
+ }
+ }
+ else
+ {
+ if (!argument.Optional)
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "Argument for -{0} can be hidden but can not be optional at the meantime.", argument.OptionName);
+ Debug.Assert(argument.Optional, message);
+ throw new ArgumentException(message);
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Get the ArgumentAttribute from the field. If the field don't define this
+ /// Custom attribute, it will return null.
+ ///
+ /// Field information.
+ /// Argument attribute associated with the field.
+ private static ArgumentAttribute GetFieldArgumentAttribute(FieldInfo fieldInfo)
+ {
+ ArgumentAttribute[] argument =
+ (ArgumentAttribute[])fieldInfo.GetCustomAttributes(typeof(ArgumentAttribute), false);
+
+ return argument.Length == 1 ? argument[0] : null;
+ }
+
+ ///
+ /// When output the usage, this function will generate the flag string
+ /// Such as "-time n1 n2..." string.
+ ///
+ /// Argument attribute.
+ /// Argument presentation on command line.
+ private static string GetFlagAndPlaceHolderString(ArgumentAttribute argument)
+ {
+ return (!string.IsNullOrEmpty(argument.OptionName) ? "-" : string.Empty) +
+ argument.OptionName +
+ (string.IsNullOrEmpty(argument.UsagePlaceholder) ?
+ string.Empty : " " + argument.UsagePlaceholder);
+ }
+
+ ///
+ /// Call by the GetFlagAndPlaceHolderString() function, and generate the frendly
+ /// Name of each parameter in command line, such as "n1 n2 ..." string.
+ ///
+ /// Field information.
+ /// Field name of the argument.
+ private static string GetFieldFriendlyTypeName(FieldInfo fieldInfo)
+ {
+ Type type = fieldInfo.FieldType.IsArray ?
+ fieldInfo.FieldType.GetElementType() : fieldInfo.FieldType;
+
+ string str;
+ if (type == typeof(bool))
+ {
+ str = string.Empty;
+ }
+ else
+ {
+ // Use the Type name's first character,
+ // for example: System.int -> i, System.double -> d
+ str = type.ToString();
+ int i = str.LastIndexOf('.');
+ i = i == -1 ? 0 : i + 1;
+ str = char.ToLower(str[i], CultureInfo.CurrentCulture).ToString();
+ }
+
+ return fieldInfo.FieldType.IsArray ? str + "1 " + str + "2 ..." : str;
+ }
+
+ ///
+ /// Check and parse the internal target and external target, then push the result
+ /// Of parsing into the DestMap.
+ /// Internal target class has predefined some flags, such as "-h", "-C"
+ /// External target class are defined by the library users.
+ ///
+ /// Target object to reflect usage information.
+ private void ParseTheDestination(object target)
+ {
+ // Check and parse the internal target, so use Debug.Assert to catch the error.
+ foreach (FieldInfo fieldInfo in typeof(InternalFlags).GetFields(AllFieldBindingFlags))
+ {
+ ArgumentAttribute argument = GetFieldArgumentAttribute(fieldInfo);
+ if (string.IsNullOrEmpty(argument.UsagePlaceholder))
+ {
+ argument.UsagePlaceholder = GetFieldFriendlyTypeName(fieldInfo);
+ }
+
+ Debug.Assert(argument != null, "Failed");
+
+ Destination destination = new Destination(fieldInfo, argument, this.internalTarget);
+
+ Debug.Assert(destination.Argument.OptionName.Length != 0, "Failed");
+ Debug.Assert(char.IsLetter(destination.Argument.OptionName[0]) || destination.Argument.OptionName[0] == '?', "Failed");
+
+ // Assert there is no duplicate flag in the user defined argument class.
+ Debug.Assert(!this.destMap.ContainsKey(destination.Argument.OptionName), "Failed");
+
+ this.destMap.Add(destination.Argument.OptionName, destination);
+ }
+
+ // Check and parse the external target, so use throw exception
+ // to handle the unexpect target difine.
+ foreach (FieldInfo fieldInfo in target.GetType().GetFields(AllFieldBindingFlags))
+ {
+ ArgumentAttribute argument = GetFieldArgumentAttribute(fieldInfo);
+ if (argument == null)
+ {
+ continue;
+ }
+
+ Destination destination = new Destination(fieldInfo, argument, null);
+
+ // Assert user don't define a non-letter as a flag in the user defined argument class.
+ if (destination.Argument.OptionName.Length > 0 && !char.IsLetter(destination.Argument.OptionName[0]))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "User can't define a non-letter flag ({0}).", destination.Argument.OptionName[0]), destination.Argument.OptionName[0].ToString());
+ }
+
+ // Assert there is no duplicate flag in the user defined argument class.
+ if (this.destMap.ContainsKey(destination.Argument.OptionName))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Duplicate flag are defined in the argument class."), destination.Argument.OptionName);
+ }
+
+ this.destMap.Add(destination.Argument.OptionName, destination);
+ }
+ }
+
+ ///
+ /// Check the given string is a flag, if so, get the corresponding destination class of the flag.
+ ///
+ /// String to test.
+ /// Destination.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Ignore.")]
+ private Destination IsFlagStringAndGetTheDestination(string str)
+ {
+ Debug.Assert(!string.IsNullOrEmpty(str), "Failed");
+
+ if (str.Length < 2 || str[0] != '-'
+ || (!char.IsLetter(str[1]) && str[1] != '?'))
+ {
+ return null;
+ }
+
+ str = str.Substring(1).ToLower(CultureInfo.InvariantCulture);
+ return this.destMap.ContainsKey(str) ? this.destMap[str] : null;
+ }
+
+ ///
+ /// Save the given string to the empty flag("") destination class.
+ ///
+ /// Flag string to save, not the realy null Flag, is the "" Flag.
+ /// Destination.
+ private Destination SaveValueStringToEmptyFlag(string str)
+ {
+ Destination destination = this.destMap.ContainsKey(string.Empty) ? this.destMap[string.Empty] : null;
+
+ if (destination == null || destination.AlreadySaved ||
+ !destination.TryToAddValue(this.clpTarget, str))
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.Append($"Unrecognized command {str}. ");
+
+ Assembly assembly = Assembly.GetEntryAssembly();
+ if (assembly != null)
+ {
+ sb.Append($"Run '{Path.GetFileName(assembly.Location)} -?' for help.");
+ }
+
+ throw new CommandLineParseException(sb.ToString(), str);
+ }
+
+ return destination;
+ }
+ }
+
+ ///
+ /// Private class, to hold the information of the target object.
+ ///
+ private class Destination
+ {
+ private FieldInfo fieldInfo;
+ private ArgumentAttribute argument;
+
+ // Hold the internal target, it distinguish
+ private InternalFlags internalTarget;
+ private bool alreadySaved;
+
+ // A internal target to a external target. If it is external, this member is null.
+ private ArrayList parameterList;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Field information.
+ /// Argument attribute.
+ /// Internal flags.
+ public Destination(FieldInfo fieldInfo, ArgumentAttribute argument, InternalFlags internalTarget)
+ {
+ this.fieldInfo = fieldInfo;
+ this.argument = argument;
+ this.internalTarget = internalTarget;
+
+ // _AlreadySaved = false;
+ this.parameterList = fieldInfo.FieldType.IsArray ? new ArrayList() : null;
+ }
+
+ ///
+ /// Gets internal target.
+ ///
+ public InternalFlags InternalTarget
+ {
+ get { return this.internalTarget; }
+ }
+
+ ///
+ /// Gets Argument attribute.
+ ///
+ public ArgumentAttribute Argument
+ {
+ get { return this.argument; }
+ }
+
+ ///
+ /// Gets a value indicating whether Value already saved.
+ ///
+ public bool AlreadySaved
+ {
+ get { return this.alreadySaved; }
+ }
+
+ ///
+ /// Parse the string to given type of value.
+ ///
+ /// Type of value.
+ /// String to parse.
+ /// Result value.
+ public static object TryParseStringToValue(Type type, string str)
+ {
+ object obj = null;
+
+ if (type == typeof(string))
+ {
+ // string to string, don't need parse.
+ obj = str;
+ }
+ else if (type == typeof(sbyte) || type == typeof(byte) ||
+ type == typeof(short) || type == typeof(ushort) ||
+ type == typeof(int) || type == typeof(uint) ||
+ type == typeof(long) || type == typeof(ulong))
+ {
+ // Use the dec style to parse the string into integer value frist.
+ // If it failed, then use the hex sytle to parse it again.
+ obj = TryParse(str, type, NumberStyles.Integer | NumberStyles.AllowThousands);
+ if (obj == null && str.Substring(0, 2) == "0x")
+ {
+ obj = TryParse(str.Substring(2), type, NumberStyles.HexNumber);
+ }
+ }
+ else if (type == typeof(double) || type == typeof(float))
+ {
+ // Use float style to parse the string into float value.
+ obj = TryParse(str, type, NumberStyles.Float | NumberStyles.AllowThousands);
+ }
+ else
+ {
+ // Use the default style to parse the string.
+ obj = TryParse(str, type);
+ }
+
+ return obj;
+ }
+
+ ///
+ /// Try to and a value to the target. Frist prase the string form parameter
+ /// To a given value. And then, save the value to a target field or a value
+ /// List.
+ ///
+ /// Target object to reflect usage information.
+ /// String value to add.
+ /// True if succeeded, otherwise false.
+ public bool TryToAddValue(object target, string str)
+ {
+ if (this.alreadySaved)
+ {
+ return false;
+ }
+
+ if (this.internalTarget != null)
+ {
+ target = this.internalTarget;
+ }
+
+ // If this field is an array, it will save the prased value into an value list.
+ // Otherwise, it will save the parse value to the field of the target directly.
+ if (this.fieldInfo.FieldType.IsArray)
+ {
+ object value = TryParseStringToValue(this.fieldInfo.FieldType.GetElementType(), str);
+ if (value == null)
+ {
+ return false;
+ }
+
+ this.parameterList.Add(value);
+ }
+ else
+ {
+ object value = TryParseStringToValue(this.fieldInfo.FieldType, str);
+ if (value == null)
+ {
+ return false;
+ }
+
+ this.fieldInfo.SetValue(target, value);
+ this.alreadySaved = true;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Save function will do some cleanup of the value save.
+ ///
+ /// Target object to reflect usage information.
+ public void Save(object target)
+ {
+ if (this.internalTarget != null)
+ {
+ target = this.internalTarget;
+ }
+
+ if (this.fieldInfo.FieldType.IsArray)
+ {
+ // When the filed is an array, this function will save all values in the ParameterList
+ // into the array field.
+ Debug.Assert(!this.alreadySaved, "Failed");
+ Array array = (Array)this.fieldInfo.GetValue(target);
+ if (array != null && array.Length != this.parameterList.Count)
+ {
+ string message = string.Format(CultureInfo.InvariantCulture, "For option flag -{0}, the parameter number is {1}, which is not as expected {2}.", this.argument.OptionName, this.parameterList.Count, array.Length);
+ throw new CommandLineParseException(message, "-" + this.argument.OptionName);
+ }
+
+ this.fieldInfo.SetValue(target, this.parameterList.ToArray(this.fieldInfo.FieldType.GetElementType()));
+ }
+ else if (this.fieldInfo.FieldType == typeof(bool))
+ {
+ if (!this.alreadySaved)
+ {
+ bool b = (bool)this.fieldInfo.GetValue(target);
+ b = !b;
+ this.fieldInfo.SetValue(target, b);
+ }
+ }
+ else if (!this.alreadySaved)
+ {
+ // Other types do nothing, only check its already saved,
+ // beacuse the value must be saved in the TryToAddValue();
+ string message = string.Format(CultureInfo.InvariantCulture, "The option flag [-{0}] needs {1} parameter.", this.argument.OptionName, this.argument.UsagePlaceholder);
+ throw new CommandLineParseException(message, "-" + this.argument.OptionName);
+ }
+
+ this.alreadySaved = true;
+ }
+
+ ///
+ /// Use the given style to parse the string to given type of value.
+ ///
+ /// String to parse.
+ /// Type of value.
+ /// Number styles.
+ /// Result value.
+ private static object TryParse(string str, Type type, NumberStyles ns)
+ {
+ // Use reflection to dynamic load the TryParse function of given type.
+ Type[] typeArgs = new Type[]
+ {
+ typeof(string),
+ typeof(NumberStyles),
+ typeof(IFormatProvider),
+ Type.GetType(type.ToString() + "&"),
+ };
+
+ MethodInfo mi = type.GetMethod("TryParse", typeArgs);
+
+ // Initilze these four parameters of the Tryparse funtion.
+ object[] objArgs = new object[]
+ {
+ str,
+ ns,
+ CultureInfo.InvariantCulture,
+ Activator.CreateInstance(type),
+ };
+
+ return DoTryParse(mi, objArgs);
+ }
+
+ ///
+ /// Use the defalut style to parse the string to given type of value.
+ ///
+ /// String to parse.
+ /// Type of value.
+ /// Result value.
+ private static object TryParse(string str, Type type)
+ {
+ // Use reflection to dynamic load the TryParse function of given type.
+ MethodInfo mi = type.GetMethod("TryParse", new Type[] { typeof(string), Type.GetType(type.ToString() + "&") });
+
+ // Initilze these two parameters of the Tryparse funtion.
+ object[] objArgs = new object[] { str, Activator.CreateInstance(type) };
+
+ return DoTryParse(mi, objArgs);
+ }
+
+ ///
+ /// Run the TryParse function by the given method and parameters.
+ ///
+ /// Method information.
+ /// Method arguments.
+ /// Result value.
+ private static object DoTryParse(MethodInfo methodInfo, object[] methodArgs)
+ {
+ object retVal = methodInfo.Invoke(null, methodArgs);
+
+ if (!(retVal is bool))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "TryParse() method must return a bool value."), methodArgs[methodArgs.Length - 1].GetType().ToString());
+ }
+
+ // the last parameter of TryParse method is a reference of a value.
+ // Therefore, it will return the last value of the parameter array.
+ // If the TryParse function failed when parsing, this DoTryParse will
+ // return null.
+ return (bool)retVal ? methodArgs[methodArgs.Length - 1] : null;
+ }
+ }
+
+ ///
+ /// This class is defined to take the internal flags, such as -h, -?, -C, and etc.
+ /// When the parse begin to parse the target object, it will parse is class's object
+ /// First. So, the parse can first put the internal flags into the DestMap to avoid
+ /// Library user redifined those flags. And When finish parsed all flags, The library
+ /// Will check the property NeedHelp to determinated those flags are appeared in the
+ /// Command line.
+ ///
+ private sealed class InternalFlags
+ {
+ [Argument("h", Description = "Help", Optional = true)]
+ private bool needHelp1;
+
+ [Argument("?", Description = "Help", Optional = true)]
+ private bool needHelp2;
+
+ [Argument("help", Description = "Help", Optional = true)]
+ private bool needHelp3;
+
+ [Argument("conf", Description = "Configuration file", Optional = true)]
+ private string configFile; // use internal instead of private to avoid unusing warning.
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public InternalFlags()
+ {
+ this.needHelp1 = this.needHelp2 = this.needHelp3 = false;
+ }
+
+ ///
+ /// Gets a value indicating whether Flag indicating whether user requires help.
+ ///
+ public bool NeedHelp
+ {
+ get { return this.needHelp1 || this.needHelp2 || this.needHelp3; }
+ }
+
+ ///
+ /// Gets or sets Configuration file path.
+ ///
+ public string ConfigFile
+ {
+ get { return this.configFile; }
+ set { this.configFile = value; }
+ }
+ }
+}
+
+///
+/// When the CommandLineParser meet an unacceptabile command line
+/// Parameter, it will throw the CommandLineParseException. If the
+/// CLP meet another arguments error by anaylse the target object,
+/// It will throw the ArgumentException defined by .NET framework.
+///
+[Serializable]
+#pragma warning disable SA1402 // File may only contain a single type
+public class CommandLineParseException : Exception
+#pragma warning restore SA1402 // File may only contain a single type
+{
+ ///
+ /// The error string is "help".
+ ///
+ public const string ErrorStringHelp = "help";
+
+ private readonly string errorString;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Message.
+ /// Error string.
+ public CommandLineParseException(string message, string error)
+ : base(message)
+ {
+ this.errorString = string.IsNullOrEmpty(error) ? string.Empty : error;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CommandLineParseException()
+ : base()
+ {
+ this.errorString = string.Empty;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Message.
+ public CommandLineParseException(string message)
+ : base(message)
+ {
+ this.errorString = string.Empty;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Message.
+ /// Inner exception.
+ public CommandLineParseException(string message, Exception inner)
+ : base(message, inner)
+ {
+ this.errorString = string.Empty;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Serialization info.
+ /// Streaming context.
+ protected CommandLineParseException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ this.errorString = string.Empty;
+ }
+
+ ///
+ /// Gets To save the error string.
+ ///
+ public string ErrorString
+ {
+ get { return this.errorString; }
+ }
+
+ ///
+ /// This method is required by serialization.
+ ///
+ /// Serialization info.
+ /// Streaming context.
+ [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ base.GetObjectData(info, context);
+ }
+}
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/CommentAttribute.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/CommentAttribute.cs
new file mode 100644
index 00000000..a182c180
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/CommentAttribute.cs
@@ -0,0 +1,46 @@
+namespace Microsoft.SpeechServices.CommonLib.CommandParser;
+
+using System;
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+public sealed class CommentAttribute : Attribute
+{
+ private readonly string headComment;
+ private readonly string rearComment = "Copyright (C) Microsoft Corporation. All rights reserved.";
+
+ public CommentAttribute(string headComment)
+ {
+ if (headComment == null)
+ {
+ throw new ArgumentNullException(nameof(headComment));
+ }
+
+ this.headComment = headComment;
+ }
+
+ public CommentAttribute(string headComment, string rearComment)
+ {
+ if (headComment == null)
+ {
+ throw new ArgumentNullException(nameof(headComment));
+ }
+
+ if (rearComment == null)
+ {
+ throw new ArgumentNullException(nameof(rearComment));
+ }
+
+ this.headComment = headComment;
+ this.rearComment = rearComment;
+ }
+
+ public string HeadComment
+ {
+ get { return this.headComment; }
+ }
+
+ public string RearComment
+ {
+ get { return this.rearComment; }
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ConsoleApp.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ConsoleApp.cs
new file mode 100644
index 00000000..f28345e4
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ConsoleApp.cs
@@ -0,0 +1,54 @@
+namespace Microsoft.SpeechServices.CommonLib.CommandParser
+{
+ using System;
+ using System.IO;
+ using System.Text;
+ using System.Threading.Tasks;
+
+ public static class ConsoleApp
+ where T : new()
+ {
+ public static async Task RunAsync(string[] arguments, Func> processAsync)
+ {
+ ArgumentNullException.ThrowIfNull(processAsync);
+
+ ArgumentNullException.ThrowIfNull(arguments);
+
+ int ret = ExitCode.NoError;
+
+ T arg = new T();
+ try
+ {
+ try
+ {
+ CommandLineParser.Parse(arguments, arg);
+ }
+ catch (CommandLineParseException cpe)
+ {
+ if (cpe.ErrorString == CommandLineParseException.ErrorStringHelp)
+ {
+ CommandLineParser.PrintUsage(arg);
+ }
+ else if (!string.IsNullOrEmpty(cpe.Message))
+ {
+ Console.WriteLine(cpe.Message);
+ }
+
+ return ExitCode.InvalidArgument;
+ }
+
+ ret = await processAsync(arg).ConfigureAwait(false);
+ return ret;
+ }
+ catch (Exception)
+ {
+ if (ret != ExitCode.NoError)
+ {
+ return ret;
+ }
+
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ExitCode.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ExitCode.cs
new file mode 100644
index 00000000..73fc25b1
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/ExitCode.cs
@@ -0,0 +1,14 @@
+namespace Microsoft.SpeechServices.CommonLib.CommandParser;
+
+public sealed class ExitCode
+{
+ public const int NoError = 0;
+
+ public const int InvalidArgument = -1;
+
+ public const int GenericError = 999;
+
+ private ExitCode()
+ {
+ }
+}
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/InOutType.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/InOutType.cs
new file mode 100644
index 00000000..9b18c63e
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommandParser/InOutType.cs
@@ -0,0 +1,12 @@
+namespace Microsoft.SpeechServices.CommonLib.CommandParser;
+
+public enum InOutType
+{
+ Unknown,
+
+ In,
+
+ Out,
+
+ InOut,
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommonLib.csproj b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommonLib.csproj
new file mode 100644
index 00000000..bd7ba814
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CommonLib.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net7.0
+ Microsoft.SpeechServices.CommonLib
+ Microsoft.SpeechServices.CommonLib
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CustomContractResolver.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CustomContractResolver.cs
new file mode 100644
index 00000000..96aaf16a
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/CustomContractResolver.cs
@@ -0,0 +1,138 @@
+namespace Microsoft.SpeechServices.CommonLib;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Serialization;
+
+public class CustomContractResolver : CamelCasePropertyNamesContractResolver
+{
+ public static readonly CustomContractResolver ReaderContractResolver = new CustomContractResolver();
+ public static readonly CustomContractResolver WriterContractResolver = new CustomContractResolver();
+
+ public static JsonSerializerSettings WriterSettings { get; } = new JsonSerializerSettings
+ {
+ ContractResolver = WriterContractResolver,
+ ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
+ Converters = new List { new StringEnumConverter() { AllowIntegerValues = false } },
+ DateFormatString = "yyyy-MM-ddTHH\\:mm\\:ss.fffZ",
+ NullValueHandling = NullValueHandling.Ignore,
+ Formatting = Formatting.Indented,
+ ReferenceLoopHandling = ReferenceLoopHandling.Ignore
+ };
+
+ public static JsonSerializerSettings ReaderSettings { get; } = new JsonSerializerSettings
+ {
+ ContractResolver = ReaderContractResolver,
+ ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
+ Converters = new List { new StringEnumConverter() { AllowIntegerValues = true } },
+ Formatting = Formatting.Indented
+ };
+
+ public static string GetResolvedPropertyName(PropertyInfo property)
+ {
+ ArgumentNullException.ThrowIfNull(property);
+
+ string propertyName;
+ var jsonAttribute = property.GetCustomAttributes(typeof(JsonPropertyAttribute)).Cast().FirstOrDefault();
+ if (jsonAttribute != null && !string.IsNullOrWhiteSpace(jsonAttribute.PropertyName))
+ {
+ propertyName = jsonAttribute.PropertyName;
+ }
+ else
+ {
+ propertyName = ReaderContractResolver.GetResolvedPropertyName(property.Name);
+ }
+
+ return propertyName;
+ }
+
+ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
+ {
+ var property = base.CreateProperty(member, memberSerialization);
+
+ if (!property.Writable)
+ {
+ var propertyInfo = member as PropertyInfo;
+ if (propertyInfo != null)
+ {
+ property.Writable = propertyInfo.CanWrite;
+ }
+ }
+
+ const string createdDateTime = "CreatedDateTime";
+ const string lastActionDateTime = "LastActionDateTime";
+ const string status = "Status";
+ const string timeToLive = "TimeToLive";
+ const string duration = "Duration";
+ const string customProperties = "CustomProperties";
+
+ if (property.PropertyType == typeof(DateTime) && property.PropertyName == this.ResolvePropertyName(createdDateTime))
+ {
+ property.ShouldSerialize =
+ instance =>
+ {
+ var value = (DateTime)instance.GetType().GetProperty(createdDateTime).GetValue(instance);
+ return value != default(DateTime);
+ };
+ }
+ else if (property.PropertyType == typeof(DateTime) && property.PropertyName == this.ResolvePropertyName(lastActionDateTime))
+ {
+ property.ShouldSerialize =
+ instance =>
+ {
+ var value = (DateTime)instance.GetType().GetProperty(lastActionDateTime).GetValue(instance);
+ return value != default(DateTime);
+ };
+ }
+ else if (property.PropertyType == typeof(OneApiState) && property.PropertyName == this.ResolvePropertyName(status))
+ {
+ property.ShouldSerialize =
+ instance =>
+ {
+ var value = (OneApiState)instance.GetType().GetProperty(status).GetValue(instance);
+ return value != default(OneApiState);
+ };
+ }
+ else if (property.PropertyType == typeof(TimeSpan) && property.PropertyName == this.ResolvePropertyName(timeToLive))
+ {
+ property.ShouldSerialize =
+ instance =>
+ {
+ var value = (TimeSpan)instance.GetType().GetProperty(timeToLive).GetValue(instance);
+ return value != TimeSpan.Zero;
+ };
+ }
+ else if (property.PropertyType == typeof(TimeSpan) && property.PropertyName == this.ResolvePropertyName(duration))
+ {
+ property.ShouldSerialize =
+ instance =>
+ {
+ var value = (TimeSpan)instance.GetType().GetProperty(duration).GetValue(instance);
+ return value != TimeSpan.Zero;
+ };
+ }
+ else if (property.PropertyType == typeof(IReadOnlyDictionary) && property.PropertyName == this.ResolvePropertyName(customProperties))
+ {
+ property.ShouldSerialize =
+ instance =>
+ {
+ var value = (IReadOnlyDictionary)instance.GetType().GetProperty(customProperties).GetValue(instance);
+ return value != null && value.Count > 0;
+ };
+ }
+
+ return property;
+ }
+
+ // do not javascriptify (camel case) dictionary keys. This would e.g. change
+ // key in artifact properties.
+ protected override string ResolveDictionaryKey(string dictionaryKey)
+ {
+ return dictionaryKey;
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/PaginatedResources.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/PaginatedResources.cs
new file mode 100644
index 00000000..43b37eb2
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/PaginatedResources.cs
@@ -0,0 +1,13 @@
+namespace Microsoft.SpeechServices.DataContracts;
+
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+public class PaginatedResources
+{
+ public IEnumerable Value { get; set; }
+
+ [JsonProperty(PropertyName = "@nextLink")]
+ public Uri NextLink { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/Public-2023-04-01-preview/StatefulResourceBase.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/Public-2023-04-01-preview/StatefulResourceBase.cs
new file mode 100644
index 00000000..f14a78b3
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/Public-2023-04-01-preview/StatefulResourceBase.cs
@@ -0,0 +1,11 @@
+namespace Microsoft.SpeechServices.DataContracts.Deprecated;
+
+using System;
+using Microsoft.SpeechServices.CommonLib.Enums;
+
+public abstract class StatefulResourceBase : StatelessResourceBase
+{
+ public OneApiState Status { get; set; }
+
+ public DateTime LastActionDateTime { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/Public-2023-04-01-preview/StatelessResourceBase.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/Public-2023-04-01-preview/StatelessResourceBase.cs
new file mode 100644
index 00000000..f0d5b29b
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/Public-2023-04-01-preview/StatelessResourceBase.cs
@@ -0,0 +1,22 @@
+namespace Microsoft.SpeechServices.DataContracts.Deprecated;
+
+using System;
+using System.ComponentModel.DataAnnotations;
+
+public abstract class StatelessResourceBase
+{
+ public Uri Self { get; set; }
+
+ [Required]
+ public string DisplayName { get; set; }
+
+ public string Description { get; set; }
+
+ public DateTime CreatedDateTime { get; set; }
+
+ public Guid ParseIdFromSelf()
+ {
+ var url = this.Self.OriginalString;
+ return Guid.Parse(url.Substring(url.LastIndexOf("/") + 1, 36));
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/ResponseBase.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/ResponseBase.cs
new file mode 100644
index 00000000..9e52725a
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/ResponseBase.cs
@@ -0,0 +1,8 @@
+namespace Microsoft.SpeechServices.DataContracts;
+
+using System;
+
+public abstract class ResponseBase
+{
+ public Uri Self { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/StatefulResourceBase.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/StatefulResourceBase.cs
new file mode 100644
index 00000000..29219196
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/StatefulResourceBase.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Cris.Http.DTOs.Public;
+
+using System;
+using Microsoft.SpeechServices.Common.Client;
+using Microsoft.SpeechServices.CommonLib.Enums;
+
+public abstract class StatefulResourceBase : StatelessResourceBase
+{
+ public OneApiState Status { get; set; }
+
+ public DateTime LastActionDateTime { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/StatelessResourceBase.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/StatelessResourceBase.cs
new file mode 100644
index 00000000..c16d78c5
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/StatelessResourceBase.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Cris.Http.DTOs.Public;
+
+using System;
+
+public abstract class StatelessResourceBase
+{
+ public string Id { get; set; }
+
+ public string DisplayName { get; set; }
+
+ public string Description { get; set; }
+
+ public DateTime CreatedDateTime { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/VoiceGeneralTaskBrief.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/VoiceGeneralTaskBrief.cs
new file mode 100644
index 00000000..3f6b4171
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/VoiceGeneralTaskBrief.cs
@@ -0,0 +1,7 @@
+using Microsoft.SpeechServices.DataContracts.Deprecated;
+
+namespace Microsoft.SpeechServices.DataContracts;
+
+public class VoiceGeneralTaskBrief : StatefulResourceBase
+{
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/VoiceGeneralTaskInputFileBase.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/VoiceGeneralTaskInputFileBase.cs
new file mode 100644
index 00000000..930595ee
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/DataContracts/VoiceGeneralTaskInputFileBase.cs
@@ -0,0 +1,20 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Cris.Http.DTOs.Public.VoiceGeneralTask;
+
+using Microsoft.SpeechServices.DataContracts;
+using System;
+
+public class VoiceGeneralTaskInputFileBase : StatefulResourceBase
+{
+ // ID is used for client to know which file responsed.
+ public Guid Id { get; set; }
+
+ public string FileContentSha256 { get; set; }
+
+ public Uri Url { get; set; }
+
+ public long? Version { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/DeploymentEnvironment.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/DeploymentEnvironment.cs
new file mode 100644
index 00000000..56f7f6e4
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/DeploymentEnvironment.cs
@@ -0,0 +1,140 @@
+namespace Microsoft.SpeechServices.CommonLib.Enums;
+
+using System;
+using System.Runtime.Serialization;
+
+[DataContract]
+public enum DeploymentEnvironment
+{
+ [EnumMember]
+ Default,
+
+ [EnumMember]
+ Local,
+
+ [EnumMember]
+ Develop,
+
+ [EnumMember]
+ DevelopEUS,
+
+ [EnumMember]
+ CanaryUSCX,
+
+ [EnumMember]
+ ProductionAUE,
+
+ [EnumMember]
+ ProductionBRS,
+
+ [EnumMember]
+ ProductionCAC,
+
+ [EnumMember]
+ ProductionUSW,
+
+ [EnumMember]
+ ProductionUSW3,
+
+ [EnumMember]
+ ProductionUSWC,
+
+ [EnumMember]
+ ProductionEA,
+
+ [EnumMember]
+ ProductionEUS,
+
+ [EnumMember]
+ ProductionEUS2,
+
+ [EnumMember]
+ ProductionFC,
+
+ [EnumMember]
+ ProductionGWC,
+
+ [EnumMember]
+ ProductionINC,
+
+ [EnumMember]
+ ProductionJINW,
+
+ [EnumMember]
+ ProductionJPE,
+
+ [EnumMember]
+ ProductionJPW,
+
+ [EnumMember]
+ ProductionKC,
+
+ [EnumMember]
+ ProductionNEU,
+
+ [EnumMember]
+ ProductionNOE,
+
+ [EnumMember]
+ ProductionQAC,
+
+ [EnumMember]
+ ProductionSAN,
+
+ [EnumMember]
+ ProductionSEA,
+
+ [EnumMember]
+ ProductionSEC,
+
+ [EnumMember]
+ ProductionSWN,
+
+ [EnumMember]
+ ProductionSWW,
+
+ [EnumMember]
+ ProductionUAEN,
+
+ [EnumMember]
+ ProductionUKS,
+
+ [EnumMember]
+ ProductionUSC,
+
+ [EnumMember]
+ ProductionUSNC,
+
+ [EnumMember]
+ ProductionUSSC,
+
+ [EnumMember]
+ ProductionWEU,
+
+ [EnumMember]
+ ProductionWUS2,
+
+ [EnumMember]
+ MooncakeChinaEast2,
+
+ [EnumMember]
+ DevelopWEU,
+
+ [EnumMember]
+ CanaryUSE2X,
+
+ [EnumMember]
+ Internal,
+
+ [EnumMember]
+ FairfaxDevOps,
+
+ [EnumMember]
+ FairfaxVirginia,
+
+ [EnumMember]
+ FairfaxArizona,
+
+ [EnumMember]
+ MooncakeDevOps,
+}
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/OneApiState.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/OneApiState.cs
new file mode 100644
index 00000000..7a0b78cb
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/OneApiState.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.SpeechServices.CommonLib.Enums;
+
+[DataContract]
+public enum OneApiState
+{
+ [Obsolete("Do not use directly - used to discover deserializer issues.")]
+ None = 0,
+
+ NotStarted,
+
+ Running,
+
+ Succeeded,
+
+ Failed,
+}
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationFileKind.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationFileKind.cs
new file mode 100644
index 00000000..1b1b512e
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationFileKind.cs
@@ -0,0 +1,10 @@
+namespace Microsoft.SpeechServices.CommonLib.Enums;
+
+public enum VideoTranslationFileKind
+{
+ None = 0,
+
+ VideoFile,
+
+ AudioFile,
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationMergeParagraphAudioAlignKind.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationMergeParagraphAudioAlignKind.cs
new file mode 100644
index 00000000..628e7a05
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationMergeParagraphAudioAlignKind.cs
@@ -0,0 +1,13 @@
+namespace Microsoft.SpeechServices.CommonLib.Enums;
+
+using System;
+
+public enum VideoTranslationMergeParagraphAudioAlignKind
+{
+ [Obsolete("Do not use directly - used to discover serializer issues.")]
+ None = 0,
+
+ TruncateIfExceed,
+
+ SpeedUpIfExceed,
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationVoiceKind.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationVoiceKind.cs
new file mode 100644
index 00000000..c7d499cc
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Enums/VideoTranslationVoiceKind.cs
@@ -0,0 +1,14 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Common.Client;
+
+public enum VideoTranslationVoiceKind
+{
+ None = 0,
+
+ PlatformVoice,
+
+ PersonalVoice,
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Extensions/FileNameExtensions.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Extensions/FileNameExtensions.cs
new file mode 100644
index 00000000..d243215f
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Extensions/FileNameExtensions.cs
@@ -0,0 +1,294 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Common;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+public static class FileNameExtensions
+{
+ public const char FileExtensionDelimiter = '.';
+
+ public const string Mp3 = "mp3";
+
+ public const string Mp4 = "mp4";
+
+ public const string CloudAudioMetadataFile = "metadata";
+
+ public const string VideoTranslationDubbingMetricsReferenceYaml = "info.yaml";
+
+ public const string CloudAudioTsvFile = "tsv";
+
+ public const string Waveform = "wav";
+
+ public const string RawWave = "raw";
+
+ public const string Ogg = "ogg";
+
+ public const string Text = "txt";
+
+ public const string Tsv = "tsv";
+
+ public const string Xml = "xml";
+
+ public const string Yaml = "yaml";
+
+ public const string Configuration = "config";
+
+ public const string CsvFile = "csv";
+
+ public const string ExcelFile = "xlsx";
+
+ public const string ZipFile = "zip";
+
+ public const string HtmlFile = "html";
+
+ public const string PngFile = "png";
+
+ public const string JpegFile = "jpeg";
+
+ public const string JsonFile = "json";
+
+ public const string Zip7Z = "7z";
+
+ public const string IniFile = "ini";
+
+ public const string LgMarkdownFile = "lg";
+
+ public const string PdfFile = "pdf";
+
+ public const string PptxFile = "pptx";
+
+ public const string WaveformRaw = "raw";
+
+ public const string SubRipFile = "srt";
+
+ public const string WebVttFile = "vtt";
+
+ public const string WebmVideoFile = "webm";
+
+ public const string M4aAudioFile = "m4a";
+
+ public const string PitchF0File = "if0";
+
+ public static string EnsureExtensionWithoutDelimiter(this string extension)
+ {
+ string extensionWithoutDelimeter = string.Empty;
+ if (!string.IsNullOrEmpty(extension))
+ {
+ if (extension[0] == FileExtensionDelimiter)
+ {
+ extensionWithoutDelimeter = extension.Substring(1);
+ }
+ else
+ {
+ extensionWithoutDelimeter = extension;
+ }
+ }
+
+ return extensionWithoutDelimeter;
+ }
+
+ public static string EnsureExtensionWithDelimiter(this string extension)
+ {
+ string extensionWithDelimiter = extension;
+ if (!string.IsNullOrEmpty(extension))
+ {
+ if (extension[0] != FileExtensionDelimiter)
+ {
+ extensionWithDelimiter = FileExtensionDelimiter + extension;
+ }
+ else
+ {
+ extensionWithDelimiter = extension;
+ }
+ }
+
+ return extensionWithDelimiter;
+ }
+
+ public static string AppendExtensionName(this string file, string extensionName)
+ {
+ extensionName = extensionName ?? string.Empty;
+ return (string.IsNullOrEmpty(extensionName) || extensionName[0] == FileExtensionDelimiter) ? file + extensionName : file + FileExtensionDelimiter + extensionName;
+ }
+
+ public static bool IsWithFileExtension(this string file, string extensionName)
+ {
+ if (string.IsNullOrEmpty(file))
+ {
+ throw new ArgumentNullException(nameof(file));
+ }
+
+ if (string.IsNullOrEmpty(extensionName))
+ {
+ throw new ArgumentNullException(nameof(extensionName));
+ }
+
+ if (extensionName[0] != FileExtensionDelimiter)
+ {
+ extensionName = FileExtensionDelimiter + extensionName;
+ }
+
+ return file.EndsWith(extensionName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public static bool IsSameFileExtension(this string actualExtension, string expectedExtension)
+ {
+ if (string.IsNullOrEmpty(actualExtension))
+ {
+ throw new ArgumentNullException(nameof(actualExtension));
+ }
+
+ if (string.IsNullOrEmpty(expectedExtension))
+ {
+ throw new ArgumentNullException(nameof(expectedExtension));
+ }
+
+ string actualExtensionWithoutDelimeter = actualExtension;
+ if (actualExtension[0] == FileExtensionDelimiter)
+ {
+ actualExtensionWithoutDelimeter = actualExtension.Substring(1);
+ }
+
+ string expectedExtensionWithoutDelimeter = expectedExtension;
+ if (expectedExtension[0] == FileExtensionDelimiter)
+ {
+ expectedExtensionWithoutDelimeter = expectedExtension.Substring(1);
+ }
+
+ bool isSame = true;
+ if (string.CompareOrdinal(actualExtensionWithoutDelimeter, expectedExtensionWithoutDelimeter) != 0)
+ {
+ isSame = false;
+ }
+
+ return isSame;
+ }
+
+ public static bool IsSupportedFileExtension(this string actualExtension, IEnumerable supportedExtensions, StringComparison stringComparison = StringComparison.CurrentCulture)
+ {
+ ArgumentNullException.ThrowIfNull(actualExtension);
+ ArgumentNullException.ThrowIfNull(supportedExtensions);
+
+ string actualExtensionWithoutDelimeter = actualExtension.EnsureExtensionWithoutDelimiter();
+ var supportedExtensionsWithoutDelimeter = supportedExtensions.Select(extension => extension.EnsureExtensionWithoutDelimiter());
+
+ return supportedExtensionsWithoutDelimeter.Any(extenstion => actualExtensionWithoutDelimeter.Equals(extenstion, stringComparison));
+ }
+
+ public static string CreateSearchPatternWithFileExtension(this string fileExtension)
+ {
+ if (string.IsNullOrEmpty(fileExtension))
+ {
+ throw new ArgumentNullException(nameof(fileExtension));
+ }
+
+ return "*".AppendExtensionName(fileExtension);
+ }
+
+ public static string RemoveFilePathExtension(this string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentNullException(nameof(filePath));
+ }
+
+ return Path.Combine(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath));
+ }
+
+ public static string ChangeFilePathExtension(this string filePath, string newFileNameExtension)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentNullException(nameof(filePath));
+ }
+
+ return Path.GetFileNameWithoutExtension(filePath).AppendExtensionName(newFileNameExtension);
+ }
+
+ public static bool HasFileExtension(this string fileName)
+ {
+ try
+ {
+ return !string.IsNullOrWhiteSpace(fileName) && !string.IsNullOrWhiteSpace(fileName.GetFileExtension());
+ }
+ catch (ArgumentException)
+ {
+ return false;
+ }
+ }
+
+ public static string GetFileExtension(this string fileName, bool withDelimiter = true)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentException("The file name is either an empty string, null or whitespace.", nameof(fileName));
+ }
+
+ try
+ {
+ var fileInfo = new FileInfo(fileName);
+
+ if (!withDelimiter)
+ {
+ if (fileInfo.Extension.StartsWith(FileExtensionDelimiter.ToString(), StringComparison.InvariantCultureIgnoreCase))
+ {
+ return fileInfo.Extension.Substring(1);
+ }
+ }
+
+ return fileInfo.Extension;
+ }
+ catch (ArgumentException)
+ {
+ return string.Empty;
+ }
+ }
+
+ public static string GetLowerCaseFileNameExtensionWithoutDot(string fileName)
+ {
+ if (string.IsNullOrEmpty(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var extension = Path.GetExtension(fileName);
+ if (string.IsNullOrEmpty(extension))
+ {
+ return extension;
+ }
+
+ extension = extension.TrimStart('.');
+#pragma warning disable CA1308 // Normalize strings to uppercase
+ return extension.ToLowerInvariant();
+#pragma warning restore CA1308 // Normalize strings to uppercase
+ }
+
+ public static string GetFileNameExtensionFromCodec(string codec)
+ {
+ var fileExtension = FileNameExtensions.Waveform;
+ if (codec.IndexOf("riff", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ fileExtension = FileNameExtensions.Waveform;
+ }
+ else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ fileExtension = FileNameExtensions.Mp3;
+ }
+ else if (codec.IndexOf("raw", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ fileExtension = FileNameExtensions.RawWave;
+ }
+ else if (codec.IndexOf("json", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ fileExtension = FileNameExtensions.JsonFile;
+ }
+
+ return fileExtension;
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Extensions/StringExtensions.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Extensions/StringExtensions.cs
new file mode 100644
index 00000000..2029aadc
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Extensions/StringExtensions.cs
@@ -0,0 +1,49 @@
+namespace Microsoft.SpeechServices.CommonLib.Extensions;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+public static class StringExtensions
+{
+ public static string MaskSasToken(this string str)
+ {
+ if (string.IsNullOrWhiteSpace(str))
+ {
+ return str;
+ }
+
+ var sasRegexPattern = "(?sig=[\\w%]+)";
+ var matches = Regex.Matches(str, sasRegexPattern);
+ foreach (Match match in matches)
+ {
+ str = str.Replace(match.Groups["signature"].Value, "SIGMASKED");
+ }
+
+ return str;
+ }
+
+ public static IReadOnlyDictionary ToDictionaryWithDelimeter(this string value)
+ {
+ var headers = new Dictionary();
+ if (!string.IsNullOrEmpty(value))
+ {
+ var headerPairs = value.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var headerPair in headerPairs.Where(x => !string.IsNullOrEmpty(x)))
+ {
+ var delimeterIndex = headerPair.IndexOf('=');
+ if (delimeterIndex < 0)
+ {
+ throw new InvalidDataException($"Invalid argument format: {value}");
+ }
+
+ headers[headerPair.Substring(0, delimeterIndex)] = headerPair.Substring(delimeterIndex + 1);
+ }
+ }
+
+ return headers;
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/HttpClientBase.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/HttpClientBase.cs
new file mode 100644
index 00000000..1e10fc89
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/HttpClientBase.cs
@@ -0,0 +1,249 @@
+namespace Microsoft.SpeechServices.CommonLib.Util;
+
+using Flurl;
+using Flurl.Http;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.CustomVoice.TtsLib.TtsUtil;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.DataContracts.Deprecated;
+using Newtonsoft.Json;
+using Polly;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+
+public abstract class HttpClientBase
+{
+ protected const string WatermarkDetectionsControllerName = "WatermarkDetections";
+ protected const string Version20230401Preview = "2023-04-01-preview";
+ private static string apiVersion = string.Empty;
+
+ public HttpClientBase(DeploymentEnvironment environment, string subKey)
+ {
+ this.Environment = environment;
+ this.SubscriptionKey = subKey;
+ }
+
+ public static string ApiVersion
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(apiVersion))
+ {
+ return apiVersion;
+ }
+
+ return Version20230401Preview;
+ }
+
+ set
+ {
+ apiVersion = value;
+ }
+ }
+
+ public virtual string RouteBase => "texttospeech";
+
+ public abstract string ControllerName { get; }
+
+ public DeploymentEnvironment Environment { get; set; }
+
+ public string SubscriptionKey { get; set; }
+
+ public Uri BaseUrl
+ {
+ get
+ {
+ return EnvironmentMetadatas.DcMetadata.GetApiBaseUrl(this.Environment);
+ }
+ }
+
+ public async Task DeleteByIdAsync(
+ Guid id,
+ IReadOnlyDictionary queryParams = null)
+ {
+ var url = this.BuildRequestBase();
+
+ url = url.AppendPathSegment(id);
+
+ if (queryParams != null)
+ {
+ foreach (var (name, value) in queryParams)
+ {
+ url = url.SetQueryParam(name, value);
+ }
+ }
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .DeleteAsync()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task QueryByIdResponseStringAsync(
+ Guid id,
+ IReadOnlyDictionary additionalHeaders = null)
+ {
+ var url = this.BuildRequestBase(additionalHeaders)
+ .AppendPathSegment(id.ToString());
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .GetAsync()
+ .ReceiveString()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ protected async Task QueryByIdAsync(
+ Guid id,
+ IReadOnlyDictionary additionalHeaders = null)
+ {
+ var url = this.BuildRequestBase(additionalHeaders)
+ .AppendPathSegment(id.ToString());
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .GetAsync()
+ .ReceiveJson()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ protected IFlurlRequest BuildRequestBase(IReadOnlyDictionary additionalHeaders = null)
+ {
+ var url = this.BaseUrl
+ .AppendPathSegment("api")
+ .AppendPathSegment(RouteBase)
+ .AppendPathSegment(this.ControllerName)
+ .SetQueryParam("api-version", ApiVersion)
+ .WithHeader("Ocp-Apim-Subscription-Key", this.SubscriptionKey);
+ if (additionalHeaders != null)
+ {
+ foreach (var additionalHeader in additionalHeaders)
+ {
+ url.WithHeader(additionalHeader.Key, additionalHeader.Value);
+ }
+ }
+
+ return url;
+ }
+
+ public async Task QueryTaskByIdUntilTerminatedAsync(
+ Guid id,
+ IReadOnlyDictionary additionalHeaders = null,
+ bool printFirstQueryResult = false,
+ TimeSpan? timeout = null)
+ where T : StatefulResourceBase
+ {
+ var startTime = DateTime.Now;
+ OneApiState? state = null;
+ var firstTimePrinted = false;
+
+ while (DateTime.Now - startTime < (timeout ?? TimeSpan.FromHours(3)))
+ {
+ var translation = await this.QueryByIdAsync(id, additionalHeaders).ConfigureAwait(false);
+ if (translation == null)
+ {
+ return null;
+ }
+
+ var runPrinted = false;
+ if (printFirstQueryResult && !firstTimePrinted)
+ {
+ runPrinted = true;
+ firstTimePrinted = true;
+ ConsoleMaskSasHelper.WriteLineMaskSas(JsonConvert.SerializeObject(
+ translation,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ }
+
+ if (new[] { OneApiState.Failed, OneApiState.Succeeded }.Contains(translation.Status))
+ {
+ Console.WriteLine();
+ Console.WriteLine();
+ Console.WriteLine($"Task completed with state: {translation.Status.AsString()}");
+ if (!runPrinted)
+ {
+ ConsoleMaskSasHelper.WriteLineMaskSas(JsonConvert.SerializeObject(
+ translation,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ }
+
+ return translation;
+ }
+ else
+ {
+ await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
+ if (state == null || state != translation.Status)
+ {
+ Console.WriteLine();
+ Console.WriteLine();
+ Console.WriteLine($"Task {translation.Status.AsString()}:");
+ state = translation.Status;
+ }
+ else
+ {
+ Console.Write(".");
+ }
+ }
+ }
+
+ Console.WriteLine();
+ Console.WriteLine();
+ Console.WriteLine($"Task run timeout after {(DateTime.Now - startTime).TotalMinutes.ToString("0.00", CultureInfo.InvariantCulture)} mins");
+ return null;
+ }
+
+ public async Task RequestWithRetryAsync(Func> requestAsyncFunc)
+ {
+ var policy = BuildRetryPolicy();
+
+ return await policy.ExecuteAsync(async () =>
+ {
+ return await ExceptionHelper.PrintHandleExceptionAsync(async () =>
+ {
+ return await requestAsyncFunc().ConfigureAwait(false);
+ });
+ });
+ }
+
+ public static Polly.Retry.AsyncRetryPolicy BuildRetryPolicy()
+ {
+ var retryPolicy = Policy
+ .Handle(IsTransientError)
+ .WaitAndRetryAsync(5, retryAttempt =>
+ {
+ var nextAttemptIn = TimeSpan.FromSeconds(5 * Math.Pow(2, retryAttempt));
+ Console.WriteLine($"Retry attempt {retryAttempt} to make request. Next try on {nextAttemptIn.TotalSeconds} seconds.");
+ return nextAttemptIn;
+ });
+
+ return retryPolicy;
+ }
+
+ protected static bool IsTransientError(FlurlHttpException exception)
+ {
+ int[] httpStatusCodesWorthRetrying =
+ {
+ (int)HttpStatusCode.RequestTimeout, // 408
+ (int)HttpStatusCode.BadGateway, // 502
+ (int)HttpStatusCode.ServiceUnavailable, // 503
+ (int)HttpStatusCode.GatewayTimeout, // 504
+ (int)HttpStatusCode.TooManyRequests, // 429
+ };
+
+ Console.WriteLine($"Flurl exception status code: {exception.StatusCode}");
+ return exception.StatusCode.HasValue && httpStatusCodesWorthRetrying.Contains(exception.StatusCode.Value);
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Readme.txt b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Readme.txt
new file mode 100644
index 00000000..c6f9e617
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Readme.txt
@@ -0,0 +1,2 @@
+Nuget:
+ 1. Not upgrade Flurl to 4.0 due to 4.0 doesn't support NewtonJson for ReceiveJson.
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/ConsoleMaskSasHelper.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/ConsoleMaskSasHelper.cs
new file mode 100644
index 00000000..a93e714e
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/ConsoleMaskSasHelper.cs
@@ -0,0 +1,25 @@
+namespace Microsoft.SpeechServices.CommonLib.Util;
+
+using Microsoft.SpeechServices.CommonLib.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+public static class ConsoleMaskSasHelper
+{
+ public static bool ShowSas { get; set; }
+
+ static ConsoleMaskSasHelper()
+ {
+ ShowSas = false;
+ }
+
+ public static void WriteLineMaskSas(string message)
+ {
+ Console.WriteLine(ShowSas ? message : message.MaskSasToken());
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/EnumExtensions.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/EnumExtensions.cs
new file mode 100644
index 00000000..31cfd792
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/EnumExtensions.cs
@@ -0,0 +1,53 @@
+namespace Microsoft.SpeechServices.CommonLib.Util;
+
+using System;
+using System.Linq;
+using System.Runtime.Serialization;
+
+public static class EnumExtensions
+{
+ public static string AsString(this TEnum enumValue)
+ where TEnum : Enum
+ {
+ if (typeof(TEnum).GetCustomAttributes(typeof(FlagsAttribute), false).Any())
+ {
+ return enumValue.ToString();
+ }
+
+ var enumMemberName = Enum.GetName(typeof(TEnum), enumValue);
+
+ var enumMember = typeof(TEnum).GetMember(enumMemberName).Single();
+ var jsonPropertyAttribute = enumMember
+ .GetCustomAttributes(typeof(DataMemberAttribute), true)
+ .Cast()
+ .SingleOrDefault();
+
+ if (jsonPropertyAttribute != null)
+ {
+ return jsonPropertyAttribute.Name;
+ }
+
+ return enumMemberName;
+ }
+
+ public static TEnum AsEnumValue(this string value)
+ {
+ return value.AsEnumValue(false);
+ }
+
+ public static TEnum AsEnumValue(this string value, bool ignoreCase)
+ {
+ var enumMembers = typeof(TEnum).GetMembers();
+ var membersAndAttributes = enumMembers
+ .Select(m => (member: m, attribute: m.GetCustomAttributes(typeof(DataMemberAttribute), true).Cast().SingleOrDefault()))
+ .Where(m => m.attribute != null)
+ .Where(m => m.attribute.Name == value);
+
+ if (membersAndAttributes.Any())
+ {
+ value = membersAndAttributes.Single().member.Name;
+ }
+
+ return (TEnum)Enum.Parse(typeof(TEnum), value, ignoreCase);
+ }
+}
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/EnvironmentMetadatas.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/EnvironmentMetadatas.cs
new file mode 100644
index 00000000..a68b8a6e
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/EnvironmentMetadatas.cs
@@ -0,0 +1,351 @@
+namespace Microsoft.SpeechServices.CommonLib.Util;
+
+using Microsoft.SpeechServices.CommonLib.Enums;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+public class EnvironmentMetadatas
+{
+ public static ReadOnlyDictionary DcMetadatas => new ReadOnlyDictionary(new Dictionary()
+ {
+ {
+ DeploymentEnvironment.Local,
+ new DcMetadata()
+ {
+ Environment = DeploymentEnvironment.Local,
+ ApiHostName = "localhost",
+ ApiPort = 44311,
+ }
+ },
+ {
+ DeploymentEnvironment.Develop,
+ new DcMetadata()
+ {
+ Environment = DeploymentEnvironment.Develop,
+ ApiHostName = "develop.customvoice.api.speech-test.microsoft.com",
+ }
+ },
+ {
+ DeploymentEnvironment.DevelopEUS,
+ new DcMetadata()
+ {
+ Environment = DeploymentEnvironment.DevelopEUS,
+ ApiHostName = "developeus.customvoice.api.speech-test.microsoft.com",
+ }
+ },
+ {
+ DeploymentEnvironment.CanaryUSCX,
+ new DcMetadata()
+ {
+ RegionIdentifier = "centraluseuap",
+ Environment = DeploymentEnvironment.CanaryUSCX,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionAUE,
+ new DcMetadata()
+ {
+ RegionIdentifier = "australiaeast",
+ Environment = DeploymentEnvironment.ProductionAUE,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionBRS,
+ new DcMetadata()
+ {
+ RegionIdentifier = "brazilsouth",
+ Environment = DeploymentEnvironment.ProductionBRS,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionCAC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "canadacentral",
+ Environment = DeploymentEnvironment.ProductionCAC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionEA,
+ new DcMetadata()
+ {
+ RegionIdentifier = "eastasia",
+ Environment = DeploymentEnvironment.ProductionEA,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionEUS,
+ new DcMetadata()
+ {
+ RegionIdentifier = "eastus",
+ Environment = DeploymentEnvironment.ProductionEUS,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionEUS2,
+ new DcMetadata()
+ {
+ RegionIdentifier = "eastus2",
+ Environment = DeploymentEnvironment.ProductionEUS2,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionFC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "francecentral",
+ Environment = DeploymentEnvironment.ProductionFC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionGWC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "germanywestcentral",
+ Environment = DeploymentEnvironment.ProductionGWC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionINC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "centralindia",
+ Environment = DeploymentEnvironment.ProductionINC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionJINW,
+ new DcMetadata()
+ {
+ RegionIdentifier = "jioindiawest",
+ Environment = DeploymentEnvironment.ProductionJINW,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionJPE,
+ new DcMetadata()
+ {
+ RegionIdentifier = "japaneast",
+ Environment = DeploymentEnvironment.ProductionJPE,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionJPW,
+ new DcMetadata()
+ {
+ RegionIdentifier = "japanwest",
+ Environment = DeploymentEnvironment.ProductionJPW,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionKC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "koreacentral",
+ Environment = DeploymentEnvironment.ProductionKC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionNEU,
+ new DcMetadata()
+ {
+ RegionIdentifier = "northeurope",
+ Environment = DeploymentEnvironment.ProductionNEU,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionNOE,
+ new DcMetadata()
+ {
+ RegionIdentifier = "norwayeast",
+ Environment = DeploymentEnvironment.ProductionNOE,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionQAC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "qatarcentral",
+ Environment = DeploymentEnvironment.ProductionQAC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionUAEN,
+ new DcMetadata()
+ {
+ RegionIdentifier = "uaenorth",
+ Environment = DeploymentEnvironment.ProductionUAEN,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionUSW3,
+ new DcMetadata()
+ {
+ RegionIdentifier = "westus3",
+ Environment = DeploymentEnvironment.ProductionUSW3,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionSAN,
+ new DcMetadata()
+ {
+ RegionIdentifier = "southafricanorth",
+ Environment = DeploymentEnvironment.ProductionSAN,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionSEA,
+ new DcMetadata()
+ {
+ RegionIdentifier = "southeastasia",
+ Environment = DeploymentEnvironment.ProductionSEA,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionSWN,
+ new DcMetadata()
+ {
+ RegionIdentifier = "switzerlandnorth",
+ Environment = DeploymentEnvironment.ProductionSWN,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionSWW,
+ new DcMetadata()
+ {
+ RegionIdentifier = "switzerlandwest",
+ Environment = DeploymentEnvironment.ProductionSWW,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionUKS,
+ new DcMetadata()
+ {
+ RegionIdentifier = "uksouth",
+ Environment = DeploymentEnvironment.ProductionUKS,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionUSC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "centralus",
+ Environment = DeploymentEnvironment.ProductionUSC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionUSNC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "northcentralus",
+ Environment = DeploymentEnvironment.ProductionUSNC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionUSSC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "southcentralus",
+ Environment = DeploymentEnvironment.ProductionUSSC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionUSW,
+ new DcMetadata()
+ {
+ RegionIdentifier = "westus",
+ Environment = DeploymentEnvironment.ProductionUSW,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionUSWC,
+ new DcMetadata()
+ {
+ RegionIdentifier = "westcentralus",
+ Environment = DeploymentEnvironment.ProductionUSWC,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionWEU,
+ new DcMetadata()
+ {
+ RegionIdentifier = "westeurope",
+ Environment = DeploymentEnvironment.ProductionWEU,
+ }
+ },
+ {
+ DeploymentEnvironment.ProductionWUS2,
+ new DcMetadata()
+ {
+ RegionIdentifier = "westus2",
+ Environment = DeploymentEnvironment.ProductionWUS2,
+ }
+ },
+ {
+ DeploymentEnvironment.MooncakeChinaEast2,
+ new DcMetadata()
+ {
+ Environment = DeploymentEnvironment.MooncakeChinaEast2,
+ RegionIdentifier = "chinaeast2",
+ }
+ }
+ });
+
+ public class DcMetadata
+ {
+ private string apiHostName = null;
+
+ public DeploymentEnvironment Environment { get; set; }
+
+ public string PortalAddress
+ {
+ get
+ {
+ var address = ApiHostName;
+ if (!string.IsNullOrEmpty(address) && ApiPort != null)
+ {
+ address = $"{address}:{ApiPort.Value}";
+ }
+
+ return address;
+ }
+ }
+
+ public int? ApiPort { get; set; }
+
+ public string RegionIdentifier { get; set; }
+
+ public string ApiHostName
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(apiHostName))
+ {
+ return apiHostName;
+ }
+ else if (!string.IsNullOrEmpty(RegionIdentifier))
+ {
+ return $"{RegionIdentifier}.customvoice.api.speech.microsoft.com";
+ }
+
+ return string.Empty;
+ }
+ set
+ {
+ apiHostName = value;
+ }
+ }
+
+ public static Uri GetApiBaseUrl(DeploymentEnvironment environment)
+ {
+ Uri url = null;
+ if (!string.IsNullOrEmpty(DcMetadatas[environment].PortalAddress))
+ {
+ url = new Uri($"https://{DcMetadatas[environment].PortalAddress}/");
+ }
+
+ return url;
+ }
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/ExceptionHelper.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/ExceptionHelper.cs
new file mode 100644
index 00000000..0a6b25af
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/ExceptionHelper.cs
@@ -0,0 +1,228 @@
+namespace Microsoft.SpeechServices.CustomVoice.TtsLib.TtsUtil;
+
+using Flurl.Http;
+using System;
+using System.Text;
+using System.Threading.Tasks;
+
+public class ExceptionHelper
+{
+ public static async Task PrintHandleExceptionAsync(Func> requestAsyncFunc)
+ {
+ ArgumentNullException.ThrowIfNull(requestAsyncFunc);
+
+ try
+ {
+ return await requestAsyncFunc().ConfigureAwait(false);
+ }
+ catch (FlurlHttpTimeoutException e)
+ {
+ Console.WriteLine($"Timeout with error: {e.Message}");
+ throw;
+ }
+ catch (FlurlHttpException ex)
+ {
+ Console.WriteLine($"{nameof(FlurlHttpException)}: {await ex.GetResponseStringAsync()}");
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
+ throw;
+ }
+ }
+
+ public static string BuildExceptionMessage(Exception exception)
+ {
+ return BuildExceptionMessage(exception, false);
+ }
+
+ public static async Task<(bool success, string error, T result)> HasRunWithoutExceptionAsync(Func> func, bool isAppendStackTrace = false)
+ {
+ if (func == null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ try
+ {
+ var result = await func().ConfigureAwait(false);
+ return (true, null, result);
+ }
+ catch (Exception e)
+ {
+ return (false, $"Failed to run function with exception: {BuildExceptionMessage(e, isAppendStackTrace)}", default(T));
+ }
+ }
+
+ public static async Task<(bool success, string error)> HasRunWithoutExceptionAsync(Func func, bool isAppendStackTrace = false)
+ {
+ if (func == null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ try
+ {
+ await func().ConfigureAwait(false);
+ return (true, null);
+ }
+ catch (Exception e)
+ {
+ return (false, $"Failed to run function with exception: {BuildExceptionMessage(e, isAppendStackTrace)}");
+ }
+ }
+
+ public static string BuildExceptionMessage(Exception exception, bool isAppendStackTrace)
+ {
+ if (exception == null)
+ {
+ throw new ArgumentNullException(nameof(exception));
+ }
+
+ var messageBuilder = new StringBuilder();
+ for (Exception current = exception; current != null; current = current.InnerException)
+ {
+ if (current.InnerException != null)
+ {
+ messageBuilder.AppendLine(current.Message);
+ }
+ else
+ {
+ messageBuilder.Append(current.Message);
+ }
+ }
+
+ if (isAppendStackTrace)
+ {
+ messageBuilder.Append(exception.StackTrace);
+ }
+
+ return messageBuilder.ToString();
+ }
+
+#pragma warning disable CA1031
+ public static (bool success, string error) HasRunWithoutException(Func<(bool success, string error)> func, bool isAppendStackTrace = false)
+ {
+ if (func == null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ try
+ {
+ return func();
+ }
+ catch (Exception e)
+ {
+ return (false, BuildExceptionMessage(e, isAppendStackTrace));
+ }
+ }
+
+ public static (bool success, string error) HasRunWithoutException(Action action, bool isAppendStackTrace = false)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ try
+ {
+ action();
+ return (true, null);
+ }
+ catch (Exception e)
+ {
+ return (false, BuildExceptionMessage(e, isAppendStackTrace));
+ }
+ }
+
+ public static (bool success, string error) HasRunWithoutExceptoin(Func<(bool success, string error)> func, bool isAppendStackTrace = false)
+ {
+ if (func == null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ try
+ {
+ return func();
+ }
+ catch (Exception e)
+ {
+ return (false, ExceptionHelper.BuildExceptionMessage(e, isAppendStackTrace));
+ }
+ }
+
+ public static (bool success, string error) HasRunWithoutExceptoin(Action action, bool isAppendStackTrace = false)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ try
+ {
+ action();
+ return (true, null);
+ }
+ catch (Exception e)
+ {
+ return (false, ExceptionHelper.BuildExceptionMessage(e, isAppendStackTrace));
+ }
+ }
+
+ public static async Task<(bool success, string error)> HasRunWithoutExceptoinAsync(Func func, bool isAppendStackTrace = false)
+ {
+ if (func == null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ try
+ {
+ await func().ConfigureAwait(false);
+ return (true, null);
+ }
+ catch (Exception e)
+ {
+ return (false, $"Failed to run function with exception: {BuildExceptionMessage(e, isAppendStackTrace)}");
+ }
+ }
+
+ public static async Task<(bool success, string error, T result)> HasRunWithoutExceptoinAsync(Func> func, bool isAppendStackTrace = false)
+ {
+ if (func == null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ try
+ {
+ var result = await func().ConfigureAwait(false);
+ return (true, null, result);
+ }
+ catch (Exception e)
+ {
+ return (false, $"Failed to run function with exception: {ExceptionHelper.BuildExceptionMessage(e, isAppendStackTrace)}", default(T));
+ }
+ }
+
+ public static (bool success, string error, T result) HasRunWithoutExceptoin(Func func, bool isAppendStackTrace = false)
+ {
+ if (func == null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ try
+ {
+ var result = func();
+ return (true, null, result);
+ }
+ catch (Exception e)
+ {
+ return (false, $"Failed to run function with exception: {ExceptionHelper.BuildExceptionMessage(e, isAppendStackTrace)}", default(T));
+ }
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/Sha256Helper.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/Sha256Helper.cs
new file mode 100644
index 00000000..39e855c3
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/Sha256Helper.cs
@@ -0,0 +1,61 @@
+namespace Microsoft.SpeechServices.CommonLib.Util;
+
+using Microsoft.SpeechServices.Common;
+using System;
+using System.Globalization;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+
+public static class Sha256Helper
+{
+ public static string GetSha256FromFile(string filename)
+ {
+ if (string.IsNullOrEmpty(filename))
+ {
+ throw new ArgumentNullException(nameof(filename));
+ }
+
+ if (!File.Exists(filename))
+ {
+ throw new FileNotFoundException(filename);
+ }
+
+ using (var md5Hash = SHA256.Create())
+ using (FileStream stream = File.OpenRead(filename))
+ {
+ byte[] data = md5Hash.ComputeHash(stream);
+ var sb = new StringBuilder();
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ sb.Append(data[i].ToString("x2", CultureInfo.InvariantCulture));
+ }
+
+ return sb.ToString();
+ }
+ }
+
+ public static string GetSha256WithExtensionFromFile(string filename)
+ {
+ return $"{GetSha256FromFile(filename).AppendExtensionName(Path.GetExtension(filename))}";
+ }
+
+ public static string GetSha256FromString(string value)
+ {
+ value = value ?? string.Empty;
+ using (var md5Hash = SHA256.Create())
+ {
+ byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(value));
+
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ sb.Append(data[i].ToString("x2", CultureInfo.InvariantCulture));
+ }
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/TaskNameHelper.cs b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/TaskNameHelper.cs
new file mode 100644
index 00000000..57320351
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/Common/CommonLib/Util/TaskNameHelper.cs
@@ -0,0 +1,56 @@
+namespace Microsoft.SpeechServices.CommonLib.Util;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+public static class TaskNameHelper
+{
+ public const int NameMaxCharLength = 256;
+ public const string IncompleteFileNamePrefix = "...";
+
+ public static string BuildAutoGeneratedFileName(IEnumerable fileNames)
+ {
+ ArgumentNullException.ThrowIfNull(fileNames);
+
+ var fileName = string.Empty;
+ if (!fileNames.Any())
+ {
+ fileName = "No file selected.";
+ }
+ else if (fileNames.Count() == 1)
+ {
+ fileName = $"{fileNames.First()}";
+ }
+ else
+ {
+ fileName = TrimFileNameToDisplayText(
+ $"{fileNames.Count()} files: {string.Join(",", fileNames)}",
+ NameMaxCharLength);
+ }
+
+ return fileName;
+ }
+
+ public static string TrimFileNameToDisplayText(string fullFileName, int maxFileNameCharCount)
+ {
+ if (string.IsNullOrEmpty(fullFileName))
+ {
+ return fullFileName;
+ }
+
+ var sb = new StringBuilder();
+ if (fullFileName.Length > maxFileNameCharCount && fullFileName.Length > IncompleteFileNamePrefix.Length)
+ {
+ sb.Append(fullFileName.Substring(fullFileName.Length - IncompleteFileNamePrefix.Length));
+ sb.Append(IncompleteFileNamePrefix);
+ }
+ else
+ {
+ sb.Append(fullFileName);
+ }
+
+ return sb.ToString();
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Arguments.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Arguments.cs
new file mode 100644
index 00000000..99268151
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Arguments.cs
@@ -0,0 +1,331 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+using Microsoft.SpeechServices.Common.Client;
+using Microsoft.SpeechServices.CommonLib.CommandParser;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.CommonLib.Extensions;
+using Microsoft.SpeechServices.VideoTranslation.Enums;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+[Comment("VideoTranslation tool.")]
+public class Arguments
+{
+ [Argument(
+ "mode",
+ Description = "Specifies the execute modes.",
+ Optional = false,
+ UsagePlaceholder = "mode",
+ RequiredModes = "QueryMetadata,UploadVideoOrAudioFile,UploadVideoOrAudioFileIfNotExist,UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation,QueryVideoOrAudioFiles,QueryTranslations,QueryVideoOrAudioFile,QueryTranslation,DeleteVideoOrAudioFile,DeleteTranslation,QueryTargetLocales,QueryTargetLocale,UpdateTargetLocaleEdittingWebvttFile,DeleteTargetLocale")]
+ private string modeString = string.Empty;
+
+ [Argument(
+ "apiVersion",
+ Description = "Specifies the api version.",
+ Optional = true,
+ UsagePlaceholder = "apiVersion",
+ OptionalModes = "QueryMetadata,UploadVideoOrAudioFile,UploadVideoOrAudioFileIfNotExist,UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation,QueryVideoOrAudioFiles,QueryTranslations,QueryVideoOrAudioFile,QueryTranslation,DeleteVideoOrAudioFile,DeleteTranslation,QueryTargetLocales,QueryTargetLocale,DeleteTargetLocale")]
+ private string apiVersion = string.Empty;
+
+ [Argument(
+ "environment",
+ Description = "Specifies the environment: ProductionEUS/CanaryUSCX/Develop/Local",
+ Optional = false,
+ UsagePlaceholder = "ProductionEUS/CanaryUSCX/Develop/Local",
+ RequiredModes = "QueryMetadata,UploadVideoOrAudioFile,UploadVideoOrAudioFileIfNotExist,UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation,QueryVideoOrAudioFiles,QueryVideoOrAudioFile,QueryTranslations,QueryTranslation,DeleteVideoOrAudioFile,DeleteTranslation,QueryTargetLocales,QueryTargetLocale,UpdateTargetLocaleEdittingWebvttFile,DeleteTargetLocale")]
+ private string environmentString = string.Empty;
+
+ [Argument(
+ "subscriptionKey",
+ Description = "Specifies speech subscription key.",
+ Optional = false,
+ UsagePlaceholder = "subscriptionKey",
+ RequiredModes = "QueryMetadata,UploadVideoOrAudioFile,UploadVideoOrAudioFileIfNotExist,UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation,QueryVideoOrAudioFiles,QueryVideoOrAudioFile,QueryTranslations,QueryTranslation,DeleteVideoOrAudioFile,DeleteTranslation,QueryTargetLocales,QueryTargetLocale,UpdateTargetLocaleEdittingWebvttFile,DeleteTargetLocale")]
+ private string subscriptionKey = string.Empty;
+
+ [Argument(
+ "id",
+ Description = "Specifies the Guid format id",
+ Optional = true,
+ UsagePlaceholder = "id",
+ RequiredModes = "QueryVideoOrAudioFile,QueryTranslation,DeleteVideoOrAudioFile,DeleteTranslation")]
+ private Guid id = Guid.Empty;
+
+ [Argument(
+ "sourceLocale",
+ Description = "Specifies the source locale: zh-CN/en-US/ru-RU/es-ES/pl-PL",
+ Optional = true,
+ UsagePlaceholder = "zh-CN/en-US/ru-RU/es-ES/pl-PL",
+ RequiredModes = "UploadVideoOrAudioFile,UploadVideoOrAudioFileIfNotExist,UploadVideoOrAudioFileAndCreateTranslation")]
+ private string sourceLocaleString = string.Empty;
+
+ [Argument(
+ "targetLocale",
+ Description = "Specifies the target locale: zh-CN/en-US/ru-RU/es-ES/pl-PL",
+ Optional = true,
+ UsagePlaceholder = "zh-CN/en-US/ru-RU/es-ES/pl-PL",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation,UpdateTargetLocaleEdittingWebvttFile,DeleteTargetLocale,QueryTargetLocale")]
+ private string targetLocaleString = string.Empty;
+
+ [Argument(
+ "targetLocales",
+ Description = "Specifies the target locales, for example: zh-CN,en-US,ru-RU,es-ES,pl-PL",
+ Optional = true,
+ UsagePlaceholder = "zh-CN,en-US,ru-RU,es-ES,pl-PL",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private string targetLocalesString = string.Empty;
+
+ [Argument(
+ "videoOrAudioFileId",
+ Description = "Specifies video or audio file ID.",
+ Optional = true,
+ UsagePlaceholder = "videoOrAudioFileId",
+ RequiredModes = "CreateTranslation,UpdateTargetLocaleEdittingWebvttFile,DeleteTargetLocale,QueryTargetLocale")]
+ private Guid videoOrAudioFileId = Guid.Empty;
+
+ [Argument(
+ "sourceVideoOrAudioFilePath",
+ Description = "Specifies path of source video or audio file.",
+ Optional = true,
+ UsagePlaceholder = "sourceVideoOrAudioFilePath",
+ RequiredModes = "UploadVideoOrAudioFile,UploadVideoOrAudioFileIfNotExist,UploadVideoOrAudioFileAndCreateTranslation")]
+ private string sourceVideoOrAudioFilePath = string.Empty;
+
+ [Argument(
+ "webvttSourceKind",
+ Description = "Specifies webvtt source kind: FileUpload(default)/TargetLocale .",
+ Optional = true,
+ UsagePlaceholder = "sourceLocaleWebvttFilePath",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private string webvttSourceKind = string.Empty;
+
+ [Argument(
+ "sourceLocaleWebvttFilePath",
+ Description = "Specifies file path of source locale webvtt.",
+ Optional = true,
+ UsagePlaceholder = "sourceLocaleWebvttFilePath",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private string sourceLocaleWebvttFilePath = string.Empty;
+
+ [Argument(
+ "targetLocaleWebvttFilePath",
+ Description = "Specifies file path of source locale webvtt.",
+ Optional = true,
+ UsagePlaceholder = "targetLocaleWebvttFilePath",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation,UpdateTargetLocaleEdittingWebvttFile")]
+ private string targetLocaleWebvttFilePath = string.Empty;
+
+ [Argument(
+ "voiceKind",
+ Description = "Specifies TTS synthesis voice kind.",
+ Optional = true,
+ UsagePlaceholder = "PlatformVoice/PersonalVoice",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private string voiceKindString = string.Empty;
+
+ [Argument(
+ "deleteAssociations",
+ Description = "Delete the video file and its associated translations.",
+ Optional = true,
+ OptionalModes = "DeleteVideoOrAudioFile,DeleteTargetLocale")]
+ private bool deleteAssociations = false;
+
+ [Argument(
+ "reuseExistingVideoOrAudioFile",
+ Description = "Whether reuse existing audio.",
+ Optional = true,
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation")]
+ private bool reuseExistingVideoOrAudioFile = false;
+
+ [Argument(
+ "withoutSubtitleInTranslatedVideoFile",
+ Description = "Whether without subtitle in translated video file.",
+ Optional = true,
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private bool withoutSubtitleInTranslatedVideoFile = false;
+
+ [Argument(
+ "subtitleMaxCharCountPerSegment",
+ Description = "Subtitle max char per segment.",
+ Optional = true,
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private int subtitleMaxCharCountPerSegment = 0;
+
+ [Argument(
+ "exportPersonalVoicePromptAudioMetadata",
+ Description = "Whether export personal voice prompt audio metadata.",
+ Optional = true,
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private bool exportPersonalVoicePromptAudioMetadata = false;
+
+ [Argument(
+ "personalVoiceModelName",
+ Description = "Personal voice model name.",
+ Optional = true,
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private string personalVoiceModelName = string.Empty;
+
+ [Argument(
+ "isAssociatedWithTargetLocale",
+ Description = "is associated with target locale.",
+ Optional = true,
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ public bool isAssociatedWithTargetLocale = false;
+
+ [Argument(
+ "additionalHttpHeaders",
+ Description = "Specifies additional http headers.",
+ Optional = false,
+ UsagePlaceholder = "name1=value1,name2=value2",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation,QueryTranslation,")]
+ private string additionalHttpHeaders = string.Empty;
+
+ [Argument(
+ "enableFeatures",
+ Description = "Specifies feature list to be enabled, supported features: GptTextReformulation",
+ Optional = false,
+ UsagePlaceholder = "GptTextReformulation,[Others]",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private string enableFeatures = string.Empty;
+
+ [Argument(
+ "profileName",
+ Description = "Specifies profile to use.",
+ Optional = false,
+ UsagePlaceholder = "GptTextReformulation,[Others]",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation")]
+ private string profileName = string.Empty;
+
+ [Argument(
+ "createTranslationAdditionalProperties",
+ Description = "Specifies additional properties when create translation.",
+ Optional = false,
+ UsagePlaceholder = "name1=value1,name2=value2",
+ OptionalModes = "UploadVideoOrAudioFileAndCreateTranslation,CreateTranslation,")]
+ private string createTranslationAdditionalProperties = string.Empty;
+
+ public string EnableFeatures => this.enableFeatures;
+
+ public string ProfileName => this.profileName;
+
+ public string ApiVersion => this.apiVersion;
+
+ public bool DeleteAssociations => this.deleteAssociations;
+
+ public bool ReuseExistingVideoOrAudioFile => this.reuseExistingVideoOrAudioFile;
+
+ public string SourceVideoOrAudioFilePath => this.sourceVideoOrAudioFilePath;
+
+ public VideoTranslationWebvttSourceKind? TypedWebvttSourceKind =>
+ string.IsNullOrWhiteSpace(this.webvttSourceKind) ? null :
+ Enum.Parse(this.webvttSourceKind);
+
+ public string SpeechSubscriptionKey => this.subscriptionKey;
+
+ public string SourceLocaleWebvttFilePath => this.sourceLocaleWebvttFilePath;
+
+ public string TargetLocaleWebvttFilePath => this.targetLocaleWebvttFilePath;
+
+ public bool WithoutSubtitleInTranslatedVideoFile => this.withoutSubtitleInTranslatedVideoFile;
+
+ public int? SubtitleMaxCharCountPerSegment => this.subtitleMaxCharCountPerSegment;
+
+ public bool ExportPersonalVoicePromptAudioMetadata => this.exportPersonalVoicePromptAudioMetadata;
+
+ public string PersonalVoiceModelName => this.personalVoiceModelName;
+
+ public bool IsAssociatedWithTargetLocale => this.isAssociatedWithTargetLocale;
+
+ public Guid Id => this.id;
+
+ public Guid VideoOrAudioFileId => this.videoOrAudioFileId;
+
+ public CultureInfo TypedSourceLocale
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(this.sourceLocaleString))
+ {
+ return null;
+ }
+
+ return CultureInfo.CreateSpecificCulture(this.sourceLocaleString);
+ }
+ }
+
+ public IReadOnlyDictionary TypedAdditionalHttpHeaders =>
+ this.additionalHttpHeaders.ToDictionaryWithDelimeter();
+
+ public IReadOnlyDictionary TypedCreateTranslationAdditionalProperties =>
+ this.createTranslationAdditionalProperties.ToDictionaryWithDelimeter();
+
+ public IEnumerable TypedTargetLocales
+ {
+ get
+ {
+ var targetLocales = new List();
+ if (!string.IsNullOrEmpty(this.targetLocalesString))
+ {
+ foreach (var targetLocaleString in this.targetLocalesString.Split(","))
+ {
+ targetLocales.Add(CultureInfo.CreateSpecificCulture(targetLocaleString));
+ }
+ }
+ else if (!string.IsNullOrEmpty(this.targetLocaleString))
+ {
+ targetLocales.Add(CultureInfo.CreateSpecificCulture(targetLocaleString));
+ }
+
+ return targetLocales;
+ }
+ }
+
+ public VideoTranslationVoiceKind? VoiceKind
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(this.voiceKindString))
+ {
+ if (Enum.TryParse(this.voiceKindString, true, out var voiceKind))
+ {
+ return voiceKind;
+ }
+ else
+ {
+ throw new NotSupportedException(this.voiceKindString);
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public DeploymentEnvironment Environment
+ {
+ get
+ {
+ if (!Enum.TryParse(this.environmentString, true, out var env))
+ {
+ throw new ArgumentException($"Invalid environment arguments.");
+ }
+
+ return env;
+ }
+ }
+
+ public Mode Mode
+ {
+ get
+ {
+ if (!Enum.TryParse(this.modeString, true, out var mode))
+ {
+ throw new ArgumentException($"Invalid mode arguments.");
+ }
+
+ return mode;
+ }
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Mode.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Mode.cs
new file mode 100644
index 00000000..94e326fd
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Mode.cs
@@ -0,0 +1,36 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+public enum Mode
+{
+ None = 0,
+
+ QueryMetadata,
+
+ UploadVideoOrAudioFile,
+
+ UploadVideoOrAudioFileIfNotExist,
+
+ UploadVideoOrAudioFileAndCreateTranslation,
+
+ CreateTranslation,
+
+ DeleteVideoOrAudioFile,
+
+ QueryVideoOrAudioFile,
+
+ QueryVideoOrAudioFiles,
+
+ DeleteTranslation,
+
+ QueryTranslation,
+
+ QueryTranslations,
+
+ QueryTargetLocales,
+
+ QueryTargetLocale,
+
+ UpdateTargetLocaleEdittingWebvttFile,
+
+ DeleteTargetLocale,
+}
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Program.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Program.cs
new file mode 100644
index 00000000..0399ce5f
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/Program.cs
@@ -0,0 +1,401 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+using Microsoft.SpeechServices.CommonLib;
+using Microsoft.SpeechServices.CommonLib.CommandParser;
+using Microsoft.SpeechServices.CommonLib.Util;
+using Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+using Microsoft.SpeechServices.VideoTranslation.Enums;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+public class Program
+{
+ static async Task Main(string[] args)
+ {
+ ConsoleMaskSasHelper.ShowSas = true;
+ return await ConsoleApp.RunAsync(
+ args,
+ ProcessAsync).ConfigureAwait(false);
+ }
+
+ public static async Task ProcessAsync(Arguments args)
+ where TVideoFileMetadata : VideoFileMetadata
+ {
+ try
+ {
+ if (!VideoTranslationConstant.SupportedEnvironments.Contains(args.Environment))
+ {
+ throw new NotSupportedException(args.Environment.AsString());
+ }
+
+ if (!string.IsNullOrEmpty(args.ApiVersion))
+ {
+ HttpClientBase.ApiVersion = args.ApiVersion;
+ }
+
+ var translationClient = new VideoTranslationClient(args.Environment, args.SpeechSubscriptionKey);
+ var fileClient = new VideoFileClient(args.Environment, args.SpeechSubscriptionKey);
+ var metadataClient = new VideoTranslationMetadataClient(args.Environment, args.SpeechSubscriptionKey);
+ var targetLocaleClient = new TargetLocaleClient(args.Environment, args.SpeechSubscriptionKey);
+
+ switch (args.Mode)
+ {
+ case Mode.QueryMetadata:
+ {
+ var metadata = await metadataClient.QueryMetadataAsync().ConfigureAwait(false);
+
+ Console.WriteLine(JsonConvert.SerializeObject(
+ metadata,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.QueryTargetLocales:
+ {
+ var targetLocales = await targetLocaleClient.QueryTargetLocalesAsync().ConfigureAwait(false);
+
+ Console.WriteLine(JsonConvert.SerializeObject(
+ targetLocales,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.QueryTargetLocale:
+ {
+ var targetLocale = await fileClient.QueryTargetLocaleAsync(
+ args.VideoOrAudioFileId,
+ args.TypedTargetLocales.First()).ConfigureAwait(false);
+
+ Console.WriteLine(JsonConvert.SerializeObject(
+ targetLocale,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.UpdateTargetLocaleEdittingWebvttFile:
+ {
+ var targetLocale = await targetLocaleClient.UpdateTargetLocaleEdittingWebvttFileAsync(
+ videoFileId: args.VideoOrAudioFileId,
+ targetLocale: args.TypedTargetLocales.First(),
+ kind: !string.IsNullOrEmpty(args.SourceLocaleWebvttFilePath) ? VideoTranslationWebVttFilePlainTextKind.SourceLocalePlainText : null,
+ webvttFilePath: !string.IsNullOrEmpty(args.SourceLocaleWebvttFilePath) ?
+ args.SourceLocaleWebvttFilePath :
+ args.TargetLocaleWebvttFilePath).ConfigureAwait(false);
+ Console.WriteLine(JsonConvert.SerializeObject(
+ targetLocale,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.DeleteTargetLocale:
+ {
+ await fileClient.DeleteTargetLocaleAsync(
+ args.VideoOrAudioFileId,
+ args.TypedTargetLocales.First(),
+ args.DeleteAssociations).ConfigureAwait(false);
+ break;
+ }
+
+ case Mode.UploadVideoOrAudioFile:
+ {
+ if (!File.Exists(args.SourceVideoOrAudioFilePath))
+ {
+ throw new FileNotFoundException(args.SourceVideoOrAudioFilePath);
+ }
+
+ Console.WriteLine($"Uploading file: {args.SourceVideoOrAudioFilePath}");
+ var videoFile = await fileClient.UploadVideoFileAsync(
+ name: Path.GetFileName(args.SourceVideoOrAudioFilePath),
+ description: null,
+ locale: args.TypedSourceLocale,
+ speakerCount: null,
+ videoFilePath: args.SourceVideoOrAudioFilePath).ConfigureAwait(false);
+
+ Console.WriteLine(JsonConvert.SerializeObject(
+ videoFile,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.UploadVideoOrAudioFileIfNotExist:
+ {
+ if (!File.Exists(args.SourceVideoOrAudioFilePath))
+ {
+ throw new FileNotFoundException(args.SourceVideoOrAudioFilePath);
+ }
+
+ var fileContentSha256 = Sha256Helper.GetSha256WithExtensionFromFile(args.SourceVideoOrAudioFilePath);
+ var videoFile = await fileClient.QueryVideoFileWithLocaleAndFileContentSha256Async(
+ args.TypedSourceLocale,
+ fileContentSha256).ConfigureAwait(false);
+ if (videoFile == null)
+ {
+ Console.WriteLine($"Uploading file: {args.SourceVideoOrAudioFilePath}");
+ videoFile = await fileClient.UploadVideoFileAsync(
+ name: Path.GetFileName(args.SourceVideoOrAudioFilePath),
+ description: null,
+ locale: args.TypedSourceLocale,
+ speakerCount: null,
+ videoFilePath: args.SourceVideoOrAudioFilePath).ConfigureAwait(false);
+ }
+
+ Console.WriteLine(JsonConvert.SerializeObject(
+ videoFile,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.UploadVideoOrAudioFileAndCreateTranslation:
+ {
+ if (string.IsNullOrEmpty(args.SourceVideoOrAudioFilePath))
+ {
+ throw new ArgumentException($"Please provide at least one of {nameof(args.VideoOrAudioFileId)} or {nameof(args.SourceVideoOrAudioFilePath)}");
+ }
+
+ if (args.TypedSourceLocale == null || string.IsNullOrEmpty(args.TypedSourceLocale.Name))
+ {
+ throw new ArgumentNullException(nameof(args.TypedSourceLocale));
+ }
+
+ if (!File.Exists(args.SourceVideoOrAudioFilePath))
+ {
+ throw new FileNotFoundException(args.SourceVideoOrAudioFilePath);
+ }
+
+ VideoFileMetadata videoOrAudioFile = null;
+ if (args.ReuseExistingVideoOrAudioFile)
+ {
+ var fileContentSha256 = Sha256Helper.GetSha256WithExtensionFromFile(args.SourceVideoOrAudioFilePath);
+ videoOrAudioFile = await fileClient.QueryVideoFileWithLocaleAndFileContentSha256Async(
+ args.TypedSourceLocale,
+ fileContentSha256).ConfigureAwait(false);
+ }
+
+ if (videoOrAudioFile == null)
+ {
+ Console.WriteLine($"Uploading file: {args.SourceVideoOrAudioFilePath}");
+ videoOrAudioFile = await fileClient.UploadVideoFileAsync(
+ name: Path.GetFileName(args.SourceVideoOrAudioFilePath),
+ description: null,
+ locale: args.TypedSourceLocale,
+ speakerCount: null,
+ videoFilePath: args.SourceVideoOrAudioFilePath).ConfigureAwait(false);
+ Console.WriteLine($"Uploaded new video file with ID {videoOrAudioFile.ParseIdFromSelf()} uploaded.");
+ }
+ else
+ {
+ Console.WriteLine($"Reuse existing video file with ID {videoOrAudioFile.ParseIdFromSelf()}.");
+ }
+
+ Console.WriteLine(JsonConvert.SerializeObject(
+ videoOrAudioFile,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+
+ var translation = await DoCreateTranslationAsync(
+ client: translationClient,
+ videoOrAudioFile: videoOrAudioFile,
+ args: args).ConfigureAwait(false);
+ if (translation == null)
+ {
+ return ExitCode.GenericError;
+ }
+
+ break;
+ }
+
+ case Mode.QueryVideoOrAudioFiles:
+ {
+ var videoFiles = await fileClient.QueryVideoFilesAsync().ConfigureAwait(false);
+ Console.WriteLine(JsonConvert.SerializeObject(
+ videoFiles,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.QueryVideoOrAudioFile:
+ {
+ var videoFile = await fileClient.QueryVideoFileAsync(args.Id).ConfigureAwait(false);
+ Console.WriteLine(JsonConvert.SerializeObject(
+ videoFile,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.DeleteVideoOrAudioFile:
+ {
+ var response = await fileClient.DeleteVideoFileAsync(args.Id, args.DeleteAssociations).ConfigureAwait(false);
+ Console.WriteLine(JsonConvert.SerializeObject(
+ ((HttpStatusCode)response.StatusCode).AsString()),
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings);
+ break;
+ }
+
+ case Mode.CreateTranslation:
+ {
+ var videoOrAudioFile = await fileClient.QueryVideoFileAsync(
+ args.VideoOrAudioFileId).ConfigureAwait(false);
+ if (videoOrAudioFile == null)
+ {
+ throw new InvalidDataException($"Failed to find video or audio file with ID {args.VideoOrAudioFileId}");
+ }
+
+ var translation = await DoCreateTranslationAsync(
+ client: translationClient,
+ videoOrAudioFile: videoOrAudioFile,
+ args: args).ConfigureAwait(false);
+ if (translation == null)
+ {
+ return ExitCode.GenericError;
+ }
+
+ break;
+ }
+
+ case Mode.QueryTranslations:
+ {
+ var translations = await translationClient.QueryTranslationsAsync().ConfigureAwait(false);
+ Console.WriteLine(JsonConvert.SerializeObject(
+ translations,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ break;
+ }
+
+ case Mode.QueryTranslation:
+ {
+ var translation = await translationClient.QueryTaskByIdUntilTerminatedAsync(
+ id: args.Id,
+ additionalHeaders: args.TypedAdditionalHttpHeaders,
+ printFirstQueryResult: true).ConfigureAwait(false);
+ if (translation == null)
+ {
+ Console.WriteLine($"Failed to find translation with ID: {args.Id}");
+ }
+
+ break;
+ }
+
+ case Mode.DeleteTranslation:
+ {
+ var response = await translationClient.DeleteTranslationAsync(args.Id).ConfigureAwait(false);
+ Console.WriteLine(JsonConvert.SerializeObject(
+ ((HttpStatusCode)response.StatusCode).AsString()),
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings);
+ break;
+ }
+
+ default:
+ throw new NotSupportedException(args.Mode.AsString());
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Failed to run with exception: {e.Message}");
+ return ExitCode.GenericError;
+ }
+
+ Console.WriteLine();
+ Console.WriteLine("Process completed successfully.");
+
+ return ExitCode.NoError;
+ }
+
+ private static async Task DoCreateTranslationAsync(
+ VideoTranslationClient client,
+ VideoFileMetadata videoOrAudioFile,
+ Arguments args)
+ {
+ var targetLocaleDefinition = new TranslationTargetLocaleCreate();
+ var filePaths = new Dictionary();
+ var targetLocales = new Dictionary();
+
+ if (args.TypedTargetLocales.Count() == 1)
+ {
+ targetLocales[args.TypedTargetLocales.Single()] = targetLocaleDefinition;
+
+ if (!string.IsNullOrEmpty(args.TargetLocaleWebvttFilePath))
+ {
+ if (!File.Exists(args.TargetLocaleWebvttFilePath))
+ {
+ throw new FileNotFoundException(args.TargetLocaleWebvttFilePath);
+ }
+
+ targetLocaleDefinition.WebVttFileName = Path.GetFileName(args.TargetLocaleWebvttFilePath);
+ filePaths[targetLocaleDefinition.WebVttFileName] = args.TargetLocaleWebvttFilePath;
+ }
+ }
+ else
+ {
+ foreach (var targetLocale in args.TypedTargetLocales)
+ {
+ targetLocales[targetLocale] = null;
+ }
+ }
+
+ var translationDefinition = new TranslationCreate()
+ {
+ VideoFileId = videoOrAudioFile.ParseIdFromSelf(),
+ VoiceKind = args.VoiceKind,
+ AlignKind = null,
+ DisplayName = $"{videoOrAudioFile.DisplayName} : {videoOrAudioFile.Locale.Name} => {string.Join(",", args.TypedTargetLocales.Select(x => x.Name))}",
+ TargetLocales = targetLocales,
+ EnableFeatures = args.EnableFeatures,
+ ProfileName = args.ProfileName,
+ PersonalVoiceModelName = args.PersonalVoiceModelName,
+ IsAssociatedWithTargetLocale = args.IsAssociatedWithTargetLocale,
+ WebvttSourceKind = args.TypedWebvttSourceKind,
+ };
+
+ translationDefinition.WithoutSubtitleInTranslatedVideoFile = args.WithoutSubtitleInTranslatedVideoFile;
+
+ translationDefinition.ExportPersonalVoicePromptAudioMetadata = args.ExportPersonalVoicePromptAudioMetadata;
+
+ var translation = await client.CreateTranslationAsync(
+ translation: translationDefinition,
+ sourceLocaleWebVttFilePath: args.SourceLocaleWebvttFilePath,
+ filePaths: filePaths,
+ additionalProperties: args.TypedCreateTranslationAdditionalProperties,
+ additionalHeaders: args.TypedAdditionalHttpHeaders).ConfigureAwait(false);
+ if (translation == null)
+ {
+ return translation;
+ }
+
+ Console.WriteLine();
+ Console.WriteLine($"New translation created with ID {translation.ParseIdFromSelf()}.");
+
+ // Print resposne of creating instead of first query state to make sure resposne of creating correct.
+ Console.WriteLine(JsonConvert.SerializeObject(
+ translation,
+ Formatting.Indented,
+ CustomContractResolver.WriterSettings));
+ var createdTranslation = await client.QueryTaskByIdUntilTerminatedAsync(
+ id: translation.ParseIdFromSelf(),
+ additionalHeaders: args.TypedAdditionalHttpHeaders,
+ printFirstQueryResult: false).ConfigureAwait(false);
+ if (createdTranslation == null)
+ {
+ Console.WriteLine($"Failed to find translation with ID: {translation.ParseIdFromSelf()}");
+ return null;
+ }
+
+ return createdTranslation;
+ }
+}
\ No newline at end of file
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/VideoTranslation.csproj b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/VideoTranslation.csproj
new file mode 100644
index 00000000..1215618e
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslation/VideoTranslation.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net7.0
+ Microsoft.SpeechServices.VideoTranslation
+ Microsoft.SpeechServices.VideoTranslation
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationApiSampleCode.sln b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationApiSampleCode.sln
new file mode 100644
index 00000000..8e9112be
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationApiSampleCode.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.6.33829.357
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoTranslationLib", "VideoTranslationLib\VideoTranslationLib.csproj", "{48AE0D58-EBDC-4517-A32B-058B17C193DB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoTranslation", "VideoTranslation\VideoTranslation.csproj", "{AEDFC024-4991-4308-A195-F82134DC6CAD}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonLib", "..\Common\CommonLib\CommonLib.csproj", "{0C202E1B-705B-4DC4-8D89-39D4EC6DB6C5}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {48AE0D58-EBDC-4517-A32B-058B17C193DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {48AE0D58-EBDC-4517-A32B-058B17C193DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {48AE0D58-EBDC-4517-A32B-058B17C193DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {48AE0D58-EBDC-4517-A32B-058B17C193DB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AEDFC024-4991-4308-A195-F82134DC6CAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AEDFC024-4991-4308-A195-F82134DC6CAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AEDFC024-4991-4308-A195-F82134DC6CAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AEDFC024-4991-4308-A195-F82134DC6CAD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C202E1B-705B-4DC4-8D89-39D4EC6DB6C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C202E1B-705B-4DC4-8D89-39D4EC6DB6C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C202E1B-705B-4DC4-8D89-39D4EC6DB6C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C202E1B-705B-4DC4-8D89-39D4EC6DB6C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6302A643-3DFD-46B4-955E-53CFF41A9F89}
+ EndGlobalSection
+EndGlobal
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/Translation.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/Translation.cs
new file mode 100644
index 00000000..8b155c09
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/Translation.cs
@@ -0,0 +1,20 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+public class Translation : TranslationBase
+{
+ public Uri InputWebVttFileUrl { get; set; }
+
+ public Uri OutputVideoSubtitleWebVttFileUrl { get; set; }
+
+ public Uri ReportFileUrl { get; set; }
+
+ public Uri IntermediateZipFileUrl { get; set; }
+
+ public Uri CacheZipFileUrl { get; set; }
+
+ public IReadOnlyDictionary TargetLocales { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationBase.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationBase.cs
new file mode 100644
index 00000000..f38604f5
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationBase.cs
@@ -0,0 +1,34 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using System;
+using System.Collections.Generic;
+using Microsoft.SpeechServices.Common.Client;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.DataContracts.Deprecated;
+using Microsoft.SpeechServices.VideoTranslation.Enums;
+
+public class TranslationBase : StatefulResourceBase
+{
+ public Guid VideoFileId { get; set; }
+
+ public VideoTranslationMergeParagraphAudioAlignKind? AudioAlignKind { get; set; }
+
+ public VideoTranslationVoiceKind? VoiceKind { get; init; }
+
+ public string EnableFeatures { get; set; }
+
+ public string ProfileName { get; set; }
+
+ public bool? WithoutSubtitleInTranslatedVideoFile { get; set; }
+
+ public bool? ExportPersonalVoicePromptAudioMetadata { get; set; }
+
+ public bool? IsAssociatedWithTargetLocale { get; set; }
+
+ public string PersonalVoiceModelName { get; set; }
+
+ public VideoTranslationWebvttSourceKind? WebvttSourceKind { get; set; }
+
+ public IReadOnlyDictionary AdditionalProperties { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationBrief.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationBrief.cs
new file mode 100644
index 00000000..adf29df9
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationBrief.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using System.Collections.Generic;
+using System.Globalization;
+
+public class TranslationBrief : TranslationBase
+{
+ public IReadOnlyDictionary TargetLocales { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationCreate.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationCreate.cs
new file mode 100644
index 00000000..cf6b9518
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationCreate.cs
@@ -0,0 +1,37 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Microsoft.SpeechServices.Common.Client;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.DataContracts.Deprecated;
+using Microsoft.SpeechServices.VideoTranslation.Enums;
+
+public class TranslationCreate : StatelessResourceBase
+{
+ public Guid VideoFileId { get; set; }
+
+ public VideoTranslationMergeParagraphAudioAlignKind? AlignKind { get; set; }
+
+ public VideoTranslationVoiceKind? VoiceKind { get; set; }
+
+ public string EnableFeatures { get; set; }
+
+ public string ProfileName { get; set; }
+
+ public int? SubtitleMaxCharCountPerSegment { get; set; }
+
+ public bool? WithoutSubtitleInTranslatedVideoFile { get; set; }
+
+ public bool? ExportPersonalVoicePromptAudioMetadata { get; set; }
+
+ public bool? IsAssociatedWithTargetLocale { get; set; }
+
+ public string PersonalVoiceModelName { get; set; }
+
+ public VideoTranslationWebvttSourceKind? WebvttSourceKind { get; set; }
+
+ public Dictionary TargetLocales { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocale.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocale.cs
new file mode 100644
index 00000000..a0b034c1
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocale.cs
@@ -0,0 +1,16 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using System;
+
+public class TranslationTargetLocale : TranslationTargetLocaleBase
+{
+ public Uri InputWebVttFileUrl { get; set; }
+
+ public Uri OutputVideoSubtitleWebVttFileUrl { get; set; }
+
+ public Uri OutputMetadataJsonWebVttFileUrl { get; set; }
+
+ public Uri OutputVideoFileUrl { get; set; }
+
+ public Uri Output24k16bitRiffAudioFileUrl { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocaleBase.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocaleBase.cs
new file mode 100644
index 00000000..18821915
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocaleBase.cs
@@ -0,0 +1,5 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+public class TranslationTargetLocaleBase
+{
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocaleCreate.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocaleCreate.cs
new file mode 100644
index 00000000..8c90fc21
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/TranslationTargetLocaleCreate.cs
@@ -0,0 +1,6 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+public class TranslationTargetLocaleCreate
+{
+ public string WebVttFileName { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileCreate.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileCreate.cs
new file mode 100644
index 00000000..9bd50be6
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileCreate.cs
@@ -0,0 +1,17 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using System.Globalization;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.DataContracts.Deprecated;
+using Microsoft.SpeechServices.VideoTranslation;
+
+public class VideoFileCreate : StatelessResourceBase
+{
+ public CultureInfo Locale { get; set; }
+
+ public int? SpeakerCount { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileMetadata.cs
new file mode 100644
index 00000000..5a9e06be
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileMetadata.cs
@@ -0,0 +1,27 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.DataContracts.Deprecated;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+public class VideoFileMetadata : StatelessResourceBase
+{
+ public VideoTranslationFileKind FileKind { get; set; }
+
+ public CultureInfo Locale { get; set; }
+
+ public int? SpeakerCount { get; set; }
+
+ public IEnumerable TargetLocales { get; set; }
+
+ public Uri VideoFileUri { get; set; }
+
+ public Uri AudioFileUri { get; set; }
+
+ public TimeSpan? Duration { get; set; }
+
+ public Uri SnapshotImageUrl { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocale.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocale.cs
new file mode 100644
index 00000000..5ff1aa27
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocale.cs
@@ -0,0 +1,13 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using Microsoft.SpeechServices.Cris.Http.DTOs.Public;
+using Microsoft.SpeechServices.Cris.Http.DTOs.Public.VideoTranslation;
+using Microsoft.SpeechServices.DataContracts;
+using System;
+using System.Globalization;
+
+// For query target locale list.
+public class VideoFileTargetLocale : VideoFileTargetLocaleBrief
+{
+ public Uri EditingMetadataJsonWebvttFileUri { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocaleBrief.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocaleBrief.cs
new file mode 100644
index 00000000..3d6efa76
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocaleBrief.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Cris.Http.DTOs.Public.VideoTranslation;
+
+using System;
+using System.Globalization;
+
+// For query target locale list.
+public class VideoFileTargetLocaleBrief : StatefulResourceBase
+{
+ public CultureInfo SourceLocale { get; set; }
+
+ public CultureInfo TargetLocale { get; set; }
+
+ public Guid VideoFileId { get; set; }
+
+ public Guid? LatestTranslationId { get; set; }
+
+ public VideoFileTargetLocaleBriefPortalProperties PortalProperties { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocaleBriefPortalProperties.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocaleBriefPortalProperties.cs
new file mode 100644
index 00000000..495b3431
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoFileTargetLocaleBriefPortalProperties.cs
@@ -0,0 +1,12 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Cris.Http.DTOs.Public.VideoTranslation;
+
+using System;
+
+public class VideoFileTargetLocaleBriefPortalProperties
+{
+ public Uri ImageUrl { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationFeatureMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationFeatureMetadata.cs
new file mode 100644
index 00000000..926b8772
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationFeatureMetadata.cs
@@ -0,0 +1,23 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using Microsoft.SpeechServices.CommonLib.Enums;
+using System.Collections.Generic;
+using System.Globalization;
+
+public class VideoTranslationFeatureMetadata
+{
+ // Use this instead of enum to avoid exception.
+ public string FeatureKind { get; set; }
+
+ public string Version { get; set; }
+
+ public string Description { get; set; }
+
+ public List SupportedSourceLocales { get; set; }
+
+ public List SupportedTargetLocales { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationMetadata.cs
new file mode 100644
index 00000000..4baf9f29
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationMetadata.cs
@@ -0,0 +1,24 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.Cris.Http.DTOs.Public.VideoTranslation;
+using Microsoft.SpeechServices.DataContracts;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+public class VideoTranslationMetadata : ResponseBase
+{
+ public Dictionary SupportedSourceLocales { get; set; }
+
+ public Dictionary SupportedTargetLocales { get; set; }
+
+ // Not use enum VideoTranslationFeatureMetadata as key to avoid client breaking when server add new enum value.
+ public List SupportedFeatures { get; set; }
+
+ public List SupportedPersonalVoiceModels { get; set; }
+
+ public List ReleaseHistoryVersions { get; set; }
+
+ public List SupportedProfiles { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationPersonalVoiceModelMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationPersonalVoiceModelMetadata.cs
new file mode 100644
index 00000000..e208f3c0
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationPersonalVoiceModelMetadata.cs
@@ -0,0 +1,10 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+public class VideoTranslationPersonalVoiceModelMetadata
+{
+ public string ModelName { get; set; }
+
+ public bool? IsDefault { get; set; }
+
+ public string Description { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationProfileMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationProfileMetadata.cs
new file mode 100644
index 00000000..23289a82
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationProfileMetadata.cs
@@ -0,0 +1,13 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+// No need expose optional features list to end user.
+public class VideoTranslationProfileMetadata
+{
+ public string Name { get; set; }
+
+ public bool? IsPrivate { get; set; }
+
+ public bool? IsDefault { get; set; }
+
+ public string Description { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationReleaseHistoryVersionMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationReleaseHistoryVersionMetadata.cs
new file mode 100644
index 00000000..937dde6c
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationReleaseHistoryVersionMetadata.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using System;
+
+public class VideoTranslationReleaseHistoryVersionMetadata
+{
+ public string Version { get; set; }
+
+ public DateTime Date { get; set; }
+
+ public string Description { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationTargetLocaleMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationTargetLocaleMetadata.cs
new file mode 100644
index 00000000..a766f08c
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/VideoTranslationTargetLocaleMetadata.cs
@@ -0,0 +1,8 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+public class VideoTranslationTargetLocaleMetadata
+{
+ public bool IsTtsPersonalVoiceSupported { get; set; }
+
+ public bool IsTtsPlatformVoiceSupported { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/WebVttFileMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/WebVttFileMetadata.cs
new file mode 100644
index 00000000..a555c1e7
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/DTOs/WebVttFileMetadata.cs
@@ -0,0 +1,13 @@
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.DataContracts.Deprecated;
+using System;
+using System.Globalization;
+
+public class WebVttFileMetadata : StatelessResourceBase
+{
+ public CultureInfo Locale { get; set; }
+
+ public Uri FileUrl { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/Utility/VideoTranslationPoolInputArgs.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/Utility/VideoTranslationPoolInputArgs.cs
new file mode 100644
index 00000000..3b52534e
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/Utility/VideoTranslationPoolInputArgs.cs
@@ -0,0 +1,50 @@
+using Microsoft.SpeechServices.Common.Client;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.VideoTranslation.Enums;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.Utility
+{
+ public class VideoTranslationPoolInputArgs
+ {
+ public VideoTranslationPoolInputArgs()
+ {
+ this.AdditionalHeaders = new Dictionary();
+ this.AdditionalProperties = new Dictionary();
+ }
+
+ public string VideoFilePath { get; set; }
+
+ public CultureInfo SourceLocale { get; set; }
+
+ public VideoTranslationVoiceKind? VoiceKind { get; set; }
+
+ public string EnableFeatures { get; set; }
+
+ public string ProfileName { get; set; }
+
+ public bool? WithoutSubtitleInTranslatedVideoFile { get; set; }
+
+ public int? SubtitleMaxCharCountPerSegment { get; set; }
+
+ public bool? ExportPersonalVoicePromptAudioMetadata { get; set; }
+
+ public string PersonalVoiceModelName { get; set; }
+
+ public bool? IsAssociatedWithTargetLocale { get; set; }
+
+ public VideoTranslationWebvttSourceKind? WebvttSourceKind { get; set; }
+
+ public List TargetLocales { get; set; }
+
+ public Dictionary AdditionalProperties { get; private set; }
+
+ public Dictionary AdditionalHeaders { get; private set; }
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/Utility/VideoTranslationPoolOutputResult.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/Utility/VideoTranslationPoolOutputResult.cs
new file mode 100644
index 00000000..ab5eaa5f
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/Utility/VideoTranslationPoolOutputResult.cs
@@ -0,0 +1,30 @@
+using Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.SpeechServices.VideoTranslation.DataContracts.Utility
+{
+ public class VideoTranslationPoolOutputResult
+ {
+ public VideoTranslationPoolInputArgs InputArgs { get; set; }
+
+ public string Error { get; set; }
+
+ public VideoFileMetadata VideoFile { get; set; }
+
+ public Translation Translation { get; set; }
+
+ public bool Success
+ {
+ get
+ {
+ return string.IsNullOrWhiteSpace(this.Error) &&
+ this.Translation?.Status == CommonLib.Enums.OneApiState.Succeeded;
+ }
+ }
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/VideoTranslationReleaseHistoryVersionMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/VideoTranslationReleaseHistoryVersionMetadata.cs
new file mode 100644
index 00000000..12486bdc
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/VideoTranslationReleaseHistoryVersionMetadata.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Cris.Http.DTOs.Public.VideoTranslation;
+
+using System;
+
+public class VideoTranslationReleaseHistoryVersionMetadata
+{
+ public string Version { get; set; }
+
+ public DateTime Date { get; set; }
+
+ public string Description { get; set; }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/VideoTranslationSourceLocaleMetadata.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/VideoTranslationSourceLocaleMetadata.cs
new file mode 100644
index 00000000..6acdea34
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/DataContracts/VideoTranslationSourceLocaleMetadata.cs
@@ -0,0 +1,9 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.Cris.Http.DTOs.Public.VideoTranslation;
+
+public class VideoTranslationSourceLocaleMetadata
+{
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/Enum/VideoTranslationWebVttFilePlainTextKind.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/Enum/VideoTranslationWebVttFilePlainTextKind.cs
new file mode 100644
index 00000000..db5ab68f
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/Enum/VideoTranslationWebVttFilePlainTextKind.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.VideoTranslation.Enums;
+
+public enum VideoTranslationWebVttFilePlainTextKind
+{
+ None = 0,
+
+ // User can upload webvtt with plain text format, the content of plain text is source locale.
+ SourceLocalePlainText,
+
+ // User can upload webvtt with plain text format, the content of plain text is target locale.
+ TargetLocalePlainText,
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/Enum/VideoTranslationWebvttSourceKind.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/Enum/VideoTranslationWebvttSourceKind.cs
new file mode 100644
index 00000000..2d8963ce
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/Enum/VideoTranslationWebvttSourceKind.cs
@@ -0,0 +1,17 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+namespace Microsoft.SpeechServices.VideoTranslation.Enums;
+
+public enum VideoTranslationWebvttSourceKind
+{
+ None = 0,
+
+ // In this kind, NOT associate with target locale with current translation.
+ FileUpload,
+
+ // 1. In this kind, will associate with target local with current translation.
+ // 2. If no user editting file, translate without webvtt file instead of response error.
+ TargetLocale,
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/TargetLocaleClient.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/TargetLocaleClient.cs
new file mode 100644
index 00000000..3aad7f54
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/TargetLocaleClient.cs
@@ -0,0 +1,83 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Flurl;
+using Flurl.Http;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+using Microsoft.SpeechServices.VideoTranslation.Enums;
+using Microsoft.SpeechServices.CommonLib.Util;
+using System.IO;
+using Microsoft.SpeechServices.Cris.Http.DTOs.Public.VideoTranslation;
+
+public class TargetLocaleClient : VideoTranslationClientBase
+{
+ public TargetLocaleClient(DeploymentEnvironment environment, string subKey)
+ : base(environment, subKey)
+ {
+ }
+
+ public override string ControllerName => "VideoFileTargetLocales";
+
+ public async Task> QueryTargetLocalesAsync(
+ int skip = 0,
+ int top = 100,
+ string orderby = "lastActionDateTime desc")
+ {
+ var url = this.BuildRequestBase();
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .SetQueryParam("skip", skip)
+ .SetQueryParam("top", top)
+ .SetQueryParam("orderby", orderby)
+ .GetAsync()
+ .ReceiveJson>()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task UpdateTargetLocaleEdittingWebvttFileAsync(
+ Guid videoFileId,
+ CultureInfo targetLocale,
+ VideoTranslationWebVttFilePlainTextKind? kind,
+ string webvttFilePath)
+ {
+ if (string.IsNullOrEmpty(webvttFilePath))
+ {
+ throw new ArgumentNullException(webvttFilePath);
+ }
+
+ if (!File.Exists(webvttFilePath))
+ {
+ throw new FileNotFoundException(webvttFilePath);
+ }
+
+ var url = this.BuildRequestBase()
+ .AppendPathSegment(videoFileId.ToString())
+ .AppendPathSegment(targetLocale.Name)
+ .AppendPathSegment("webvtt");
+ if ((kind ?? VideoTranslationWebVttFilePlainTextKind.None) != VideoTranslationWebVttFilePlainTextKind.None)
+ {
+ url = url.SetQueryParam("kind", kind.Value.AsString());
+ }
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .PostMultipartAsync(mp =>
+ {
+ mp.AddFile("webVttFile", webvttFilePath);
+ });
+ })
+ .ReceiveJson()
+ .ConfigureAwait(false);
+ }
+
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoFileClient.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoFileClient.cs
new file mode 100644
index 00000000..58f94d5f
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoFileClient.cs
@@ -0,0 +1,268 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Flurl;
+using Flurl.Http;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+
+public class VideoFileClient : VideoTranslationClientBase
+{
+ public VideoFileClient(DeploymentEnvironment environment, string subKey)
+ : base(environment, subKey)
+ {
+ }
+
+ public override string ControllerName => "VideoFiles";
+
+ public async Task QueryTargetLocaleAsync(
+ Guid videoOrAudioFileId,
+ CultureInfo targetLocale)
+ {
+ var url = this.BuildRequestBase();
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ //var content = await url
+ // .AppendPathSegment(videoOrAudioFileId)
+ // .AppendPathSegment(targetLocale.Name)
+ // .GetAsync()
+ // .ReceiveString()
+ // .ConfigureAwait(false);
+ return await url
+ .AppendPathSegment(videoOrAudioFileId)
+ .AppendPathSegment(targetLocale.Name)
+ .GetAsync()
+ .ReceiveJson()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task UploadVideoFileWithStringResponseAsync(
+ string name,
+ string description,
+ CultureInfo locale,
+ int? speakerCount,
+ string videoFilePath)
+ {
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ var response = this.PostUploadVideoFileWithResponseAsync(
+ name: name,
+ description: description,
+ locale: locale,
+ speakerCount: speakerCount,
+ videoFilePath: videoFilePath);
+
+ return await response
+ .ReceiveString();
+ }).ConfigureAwait(false);
+ }
+
+ public async Task UploadVideoFileAsync(
+ string name,
+ string description,
+ CultureInfo locale,
+ int? speakerCount,
+ string videoFilePath)
+ where TVideoFileMetadata : VideoFileMetadata
+ {
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ var response = this.PostUploadVideoFileWithResponseAsync(
+ name: name,
+ description: description,
+ locale: locale,
+ speakerCount: speakerCount,
+ videoFilePath: videoFilePath);
+
+ return await response
+ .ReceiveJson();
+ }).ConfigureAwait(false);
+ }
+
+ public async Task> QueryVideoFilesAsync()
+ {
+ var url = this.BuildRequestBase();
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url.GetAsync()
+ .ReceiveJson>()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task DeleteVideoFileAsync(
+ Guid videoFileId,
+ bool deleteAssociations)
+ {
+ var queryParams = new Dictionary();
+ if (deleteAssociations)
+ {
+ queryParams["deleteAssociations"] = bool.TrueString;
+ }
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await this.DeleteByIdAsync(videoFileId, queryParams).ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task> QueryTranslationsAsync(Guid videoFileId)
+ {
+ var url = this.BuildRequestBase()
+ .AppendPathSegment(videoFileId.ToString())
+ .AppendPathSegment("translations");
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .GetAsync()
+ .ReceiveJson>()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task DeleteTargetLocaleAsync(
+ Guid videoFileId,
+ CultureInfo locale,
+ bool deleteAssociations)
+ {
+ var url = this.BuildRequestBase()
+ .AppendPathSegment(videoFileId.ToString())
+ .AppendPathSegment(locale.Name);
+ if (deleteAssociations)
+ {
+ url = url.SetQueryParam("deleteAssociations", deleteAssociations);
+ }
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .DeleteAsync()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task QueryTargetLocaleWebVttAsync(
+ Guid videoFileId,
+ CultureInfo locale)
+ {
+ var url = this.BuildRequestBase()
+ .AppendPathSegment(videoFileId.ToString())
+ .AppendPathSegment(locale.Name)
+ .AppendPathSegment("webvtt");
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .GetAsync()
+ .ReceiveJson()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task QueryVideoFileWithLocaleAndFileContentSha256Async(
+ CultureInfo locale,
+ string fileContentSha256)
+ {
+ if (locale == null || string.IsNullOrEmpty(locale.Name))
+ {
+ throw new ArgumentNullException(nameof(locale));
+ }
+
+ if (string.IsNullOrEmpty(fileContentSha256))
+ {
+ throw new ArgumentNullException(nameof(fileContentSha256));
+ }
+
+ var url = this.BuildRequestBase()
+ .AppendPathSegment("QueryByFileContentSha256")
+ .SetQueryParam("locale", locale.Name)
+ .SetQueryParam("fileContentSha256", fileContentSha256);
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ try
+ {
+ return await url
+ .GetAsync()
+ .ReceiveJson()
+ .ConfigureAwait(false);
+ }
+ catch (FlurlHttpException ex)
+ {
+ if (ex.StatusCode == (int)HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+
+ Console.Write($"Response failed with error: {await ex.GetResponseStringAsync().ConfigureAwait(false)}");
+ throw;
+ }
+ }).ConfigureAwait(false);
+ }
+
+ public async Task QueryVideoFileAsync(Guid id)
+ where TVideoFileMetadata : VideoFileMetadata
+ {
+ var url = this.BuildRequestBase();
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .AppendPathSegment(id.ToString())
+ .GetAsync()
+ .ReceiveJson()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ private async Task PostUploadVideoFileWithResponseAsync(
+ string name,
+ string description,
+ CultureInfo locale,
+ int? speakerCount,
+ string videoFilePath)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var url = this.BuildRequestBase();
+
+ url.ConfigureRequest(settings => settings.Timeout = VideoTranslationConstant.UploadVideoOrAudioFileTimeout);
+
+ return await url
+ .PostMultipartAsync(mp =>
+ {
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ mp.AddString(nameof(VideoFileMetadata.DisplayName), name);
+ }
+
+ if (!string.IsNullOrWhiteSpace(description))
+ {
+ mp.AddString(nameof(VideoFileMetadata.Description), description);
+ }
+
+ if (locale != null && !string.IsNullOrEmpty(locale.Name))
+ {
+ mp.AddString(nameof(VideoFileMetadata.Locale), locale.Name);
+ }
+
+ if (speakerCount != null)
+ {
+ mp.AddString(nameof(VideoFileMetadata.SpeakerCount), speakerCount.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ mp.AddFile("videoFile", videoFilePath);
+ });
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationClient.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationClient.cs
new file mode 100644
index 00000000..042cf326
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationClient.cs
@@ -0,0 +1,278 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+using Flurl;
+using Flurl.Http;
+using Microsoft.SpeechServices.Common.Client;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.CommonLib.Util;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+using Newtonsoft.Json;
+using Polly;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+public class VideoTranslationClient : VideoTranslationClientBase
+{
+ public VideoTranslationClient(DeploymentEnvironment environment, string subKey)
+ : base(environment, subKey)
+ {
+ }
+
+ public override string ControllerName => "VideoTranslations";
+
+ public async Task DeleteTranslationAsync(
+ Guid translationId)
+ {
+ var url = this.BuildRequestBase()
+ .AppendPathSegment(translationId.ToString());
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url
+ .DeleteAsync()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task QueryTranslationAsync(
+ Guid id,
+ IReadOnlyDictionary additionalHeaders)
+ {
+ var url = this.BuildRequestBase();
+
+ url = url.AppendPathSegment(id.ToString());
+
+ if (additionalHeaders != null)
+ {
+ foreach (var additionalHeader in additionalHeaders)
+ {
+ url.WithHeader(additionalHeader.Key, additionalHeader.Value);
+ }
+ }
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ try
+ {
+ return await url
+ .GetAsync()
+ .ReceiveJson()
+ .ConfigureAwait(false);
+ }
+ catch (FlurlHttpException ex)
+ {
+ if (ex.StatusCode == (int)HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+
+ Console.Write($"Response failed with error: {await ex.GetResponseStringAsync().ConfigureAwait(false)}");
+ throw;
+ }
+ }).ConfigureAwait(false);
+ }
+
+ public async Task> QueryTranslationsAsync()
+ {
+ var url = this.BuildRequestBase();
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ // var responseJson = await url.GetStringAsync().ConfigureAwait(false);
+ return await url.GetAsync()
+ .ReceiveJson>()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task CreateTranslationAsync(
+ TranslationCreate translation,
+ string sourceLocaleWebVttFilePath,
+ IReadOnlyDictionary filePaths,
+ IReadOnlyDictionary additionalProperties = null,
+ IReadOnlyDictionary additionalHeaders = null)
+ {
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ var response = this.CreateTranslationWithResponseAsync(
+ translation: translation,
+ sourceLocaleWebVttFilePath: sourceLocaleWebVttFilePath,
+ filePaths: filePaths,
+ additionalProperties: additionalProperties,
+ additionalHeaders: additionalHeaders);
+ return await response.ReceiveJson()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ public async Task CreateTranslationWithStringResponseAsync(
+ TranslationCreate translation,
+ string sourceLocaleWebVttFilePath,
+ IReadOnlyDictionary filePaths,
+ IReadOnlyDictionary additionalProperties = null,
+ IReadOnlyDictionary additionalHeaders = null)
+ {
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ var response = this.CreateTranslationWithResponseAsync(
+ translation: translation,
+ sourceLocaleWebVttFilePath: sourceLocaleWebVttFilePath,
+ filePaths: filePaths,
+ additionalProperties: additionalProperties,
+ additionalHeaders: additionalHeaders);
+ return await response.ReceiveString()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+
+ private async Task CreateTranslationWithResponseAsync(
+ TranslationCreate translation,
+ string sourceLocaleWebVttFilePath,
+ IReadOnlyDictionary filePaths,
+ IReadOnlyDictionary additionalProperties = null,
+ IReadOnlyDictionary additionalHeaders = null)
+ {
+ ArgumentNullException.ThrowIfNull(translation);
+
+ if (string.IsNullOrWhiteSpace(translation.DisplayName))
+ {
+ throw new ArgumentNullException(nameof(translation.DisplayName));
+ }
+
+ if (translation.TargetLocales != null && filePaths != null)
+ {
+ foreach (var targetLocale in translation.TargetLocales.Where(x => !string.IsNullOrEmpty(x.Value?.WebVttFileName)))
+ {
+ if (!filePaths.ContainsKey(targetLocale.Value.WebVttFileName))
+ {
+ throw new InvalidDataException($"Not found file {targetLocale.Value.WebVttFileName} for locale {targetLocale.Key}");
+ }
+
+ if (!File.Exists(filePaths[targetLocale.Value.WebVttFileName]))
+ {
+ throw new FileNotFoundException(filePaths[targetLocale.Value.WebVttFileName]);
+ }
+ }
+ }
+
+ var url = this.BuildRequestBase();
+
+ if (additionalHeaders != null)
+ {
+ foreach (var additionalHeader in additionalHeaders)
+ {
+ url.WithHeader(additionalHeader.Key, additionalHeader.Value);
+ }
+ }
+
+ return await url.PostMultipartAsync(mp =>
+ {
+ if (!string.IsNullOrWhiteSpace(translation.DisplayName))
+ {
+ mp.AddString(nameof(TranslationCreate.DisplayName), translation.DisplayName);
+ }
+
+ if (!string.IsNullOrWhiteSpace(translation.Description))
+ {
+ mp.AddString(nameof(TranslationCreate.Description), translation.Description);
+ }
+
+ if (!string.IsNullOrEmpty(translation.EnableFeatures))
+ {
+ mp.AddString(nameof(TranslationCreate.EnableFeatures), translation.EnableFeatures);
+ }
+
+ if (!string.IsNullOrEmpty(translation.ProfileName))
+ {
+ mp.AddString(nameof(TranslationCreate.ProfileName), translation.ProfileName);
+ }
+
+ if (!string.IsNullOrEmpty(translation.ProfileName))
+ {
+ mp.AddString(nameof(TranslationCreate.ProfileName), translation.ProfileName);
+ }
+
+ if (translation.AlignKind != null)
+ {
+ mp.AddString(nameof(TranslationCreate.AlignKind), translation.AlignKind.Value.AsString());
+ }
+
+ if (translation.VoiceKind != null)
+ {
+ mp.AddString(nameof(TranslationCreate.VoiceKind), translation.VoiceKind.Value.AsString());
+ }
+
+ if (translation.WithoutSubtitleInTranslatedVideoFile ?? false)
+ {
+ mp.AddString(nameof(TranslationCreate.WithoutSubtitleInTranslatedVideoFile),
+ translation.WithoutSubtitleInTranslatedVideoFile.Value.ToString());
+ }
+
+ if (translation.SubtitleMaxCharCountPerSegment != null)
+ {
+ mp.AddString(nameof(TranslationCreate.SubtitleMaxCharCountPerSegment),
+ translation.SubtitleMaxCharCountPerSegment.Value.ToString());
+ }
+
+ if (translation.ExportPersonalVoicePromptAudioMetadata ?? false)
+ {
+ mp.AddString(nameof(TranslationCreate.ExportPersonalVoicePromptAudioMetadata),
+ translation.ExportPersonalVoicePromptAudioMetadata.Value.ToString());
+ }
+
+ if (!string.IsNullOrEmpty(translation.PersonalVoiceModelName))
+ {
+ mp.AddString(nameof(TranslationCreate.PersonalVoiceModelName),
+ translation.PersonalVoiceModelName);
+ }
+
+ if (translation.IsAssociatedWithTargetLocale ?? false)
+ {
+ mp.AddString(nameof(TranslationCreate.IsAssociatedWithTargetLocale),
+ translation.IsAssociatedWithTargetLocale.Value.ToString());
+ }
+
+ if (translation.WebvttSourceKind != null)
+ {
+ mp.AddString(nameof(TranslationCreate.WebvttSourceKind),
+ translation.WebvttSourceKind.Value.AsString());
+ }
+
+ if (additionalProperties != null)
+ {
+ foreach (var (name, value) in additionalProperties)
+ {
+ mp.AddString(name, value);
+ }
+ }
+
+ if (!string.IsNullOrEmpty(sourceLocaleWebVttFilePath))
+ {
+ if (!File.Exists(sourceLocaleWebVttFilePath))
+ {
+ throw new FileNotFoundException(sourceLocaleWebVttFilePath);
+ }
+
+ mp.AddFile("sourceLocaleWebVttFile", sourceLocaleWebVttFilePath, fileName: Path.GetFileName(sourceLocaleWebVttFilePath));
+ }
+
+ mp.AddJson(nameof(TranslationCreate.TargetLocales) + "JsonString", translation.TargetLocales);
+
+ mp.AddString(nameof(TranslationCreate.VideoFileId), translation.VideoFileId.ToString());
+ if (filePaths != null)
+ {
+ foreach (var (name, path) in filePaths.Where(x => translation.TargetLocales.Any(y =>
+ string.Equals(x.Key, y.Value.WebVttFileName, StringComparison.Ordinal))))
+ {
+ mp.AddFile("files", path, fileName: name);
+ }
+ }
+ }).ConfigureAwait(false);
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationClientBase.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationClientBase.cs
new file mode 100644
index 00000000..72971838
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationClientBase.cs
@@ -0,0 +1,13 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.CommonLib.Util;
+
+public abstract class VideoTranslationClientBase : HttpClientBase
+{
+ public VideoTranslationClientBase(DeploymentEnvironment environment, string subKey)
+ : base(environment, subKey)
+ {
+ }
+
+ public override string RouteBase => "videotranslation";
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationConstant.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationConstant.cs
new file mode 100644
index 00000000..9c3efa95
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationConstant.cs
@@ -0,0 +1,19 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+using System;
+using System.Collections.Generic;
+using Microsoft.SpeechServices.CommonLib.Enums;
+
+public static class VideoTranslationConstant
+{
+ public readonly static TimeSpan UploadVideoOrAudioFileTimeout = TimeSpan.FromMinutes(10);
+
+ public readonly static IEnumerable SupportedEnvironments = new[]
+ {
+ DeploymentEnvironment.Local,
+ DeploymentEnvironment.Develop,
+ DeploymentEnvironment.DevelopEUS,
+ DeploymentEnvironment.CanaryUSCX,
+ DeploymentEnvironment.ProductionEUS,
+ };
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationLib.csproj b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationLib.csproj
new file mode 100644
index 00000000..e234c164
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationLib.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net7.0
+ Microsoft.SpeechServices.$(MSBuildProjectName)
+ Microsoft.SpeechServices.VideoTranslation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationMetadataClient.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationMetadataClient.cs
new file mode 100644
index 00000000..d07e297d
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationMetadataClient.cs
@@ -0,0 +1,40 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+using Flurl;
+using Flurl.Http;
+using Microsoft.SpeechServices.Common.Client;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.CommonLib.Util;
+using Microsoft.SpeechServices.DataContracts;
+using Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+using Newtonsoft.Json;
+using Polly;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+public class VideoTranslationMetadataClient : VideoTranslationClientBase
+{
+ public VideoTranslationMetadataClient(DeploymentEnvironment environment, string subKey)
+ : base(environment, subKey)
+ {
+ }
+
+ public override string ControllerName => "VideoTranslationMetadata";
+
+ public async Task QueryMetadataAsync()
+ {
+ var url = this.BuildRequestBase();
+
+ return await this.RequestWithRetryAsync(async () =>
+ {
+ return await url.GetAsync()
+ .ReceiveJson()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationPool.cs b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationPool.cs
new file mode 100644
index 00000000..289581cb
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/VideoTranslationApiSampleCode/VideoTranslationLib/VideoTranslationPool.cs
@@ -0,0 +1,159 @@
+namespace Microsoft.SpeechServices.VideoTranslation;
+
+using Flurl.Util;
+using Microsoft.SpeechServices.CommonLib.Enums;
+using Microsoft.SpeechServices.CommonLib.Util;
+using Microsoft.SpeechServices.CustomVoice.TtsLib.TtsUtil;
+using Microsoft.SpeechServices.VideoTranslation.DataContracts.DTOs;
+using Microsoft.SpeechServices.VideoTranslation.DataContracts.Utility;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
+
+public class VideoTranslationPool
+ where TVideoFileMetadata : VideoFileMetadata
+{
+ private VideoTranslationPool()
+ {
+ this.TranslationResults = new ConcurrentDictionary();
+
+ this.waitTranslationExecutionActionBlock = new ActionBlock<(Guid translationId, IReadOnlyDictionary additionalHeaders)>(
+ async (args) =>
+ {
+ try
+ {
+ var translation = await this.TranslationClient.QueryTaskByIdUntilTerminatedAsync(
+ id: args.translationId,
+ additionalHeaders: args.additionalHeaders,
+ printFirstQueryResult: false,
+ timeout: TimeSpan.FromHours(3)).ConfigureAwait(false);
+ this.TranslationResults[args.translationId].Translation = translation;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(ExceptionHelper.BuildExceptionMessage(e, true));
+ throw;
+ }
+ },
+ new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = 2 });
+
+ this.createTranslationActionBlock = new ActionBlock(async (args) =>
+ {
+ var result = new VideoTranslationPoolOutputResult()
+ {
+ InputArgs = args,
+ };
+
+ try
+ {
+ var videoFileContentSha256 = Sha256Helper.GetSha256WithExtensionFromFile(args.VideoFilePath);
+ var videoFile = await this.FileClient.QueryVideoFileWithLocaleAndFileContentSha256Async(
+ args.SourceLocale,
+ videoFileContentSha256).ConfigureAwait(false);
+ if (videoFile == null)
+ {
+ Console.WriteLine($"Uploading file: {args.VideoFilePath}");
+ videoFile = await this.FileClient.UploadVideoFileAsync(
+ name: Path.GetFileName(args.VideoFilePath),
+ description: null,
+ locale: args.SourceLocale,
+ speakerCount: null,
+ videoFilePath: args.VideoFilePath).ConfigureAwait(false);
+ }
+
+ result.VideoFile = videoFile;
+
+ var translationCreate = new TranslationCreate()
+ {
+ DisplayName = $"{videoFile.DisplayName} : {videoFile.Locale.Name} => {string.Join(",", args.TargetLocales.Select(x => x.Name))}",
+ Description = this.TranslatoinDescription,
+ VideoFileId = videoFile.ParseIdFromSelf(),
+ EnableFeatures = args.EnableFeatures,
+ ProfileName = args.ProfileName,
+ WithoutSubtitleInTranslatedVideoFile = args.WithoutSubtitleInTranslatedVideoFile,
+ SubtitleMaxCharCountPerSegment = args.SubtitleMaxCharCountPerSegment,
+ ExportPersonalVoicePromptAudioMetadata = args.ExportPersonalVoicePromptAudioMetadata,
+ PersonalVoiceModelName = args.PersonalVoiceModelName,
+ IsAssociatedWithTargetLocale = args.IsAssociatedWithTargetLocale,
+ WebvttSourceKind = args.WebvttSourceKind,
+ VoiceKind = args.VoiceKind,
+ TargetLocales = args.TargetLocales.ToDictionary(x => x, x => (TranslationTargetLocaleCreate)null),
+ };
+
+ result.Translation = await this.TranslationClient.CreateTranslationAsync(
+ translation: translationCreate,
+ sourceLocaleWebVttFilePath: null,
+ filePaths: null,
+ additionalProperties: args.AdditionalProperties,
+ additionalHeaders: args.AdditionalHeaders).ConfigureAwait(false);
+
+ var translationId = result.Translation.ParseIdFromSelf();
+ this.TranslationResults[translationId] = result;
+
+ this.PostWaitTranslationAction(translationId, args.AdditionalHeaders);
+ }
+ catch (Exception e)
+ {
+ result.Error = $"Caught Exception with error: {e.Message}";
+ Console.WriteLine(ExceptionHelper.BuildExceptionMessage(e, true));
+ }
+
+ },
+ new ExecutionDataflowBlockOptions()
+ {
+ MaxDegreeOfParallelism = 50,
+ });
+ }
+
+ public void PostWaitTranslationAction(Guid translationId, IReadOnlyDictionary additionalHeaders)
+ {
+ this.waitTranslationExecutionActionBlock.Post((translationId, additionalHeaders));
+ }
+
+ public VideoTranslationClient TranslationClient { get; private set; }
+
+ public VideoFileClient FileClient { get; private set; }
+
+ public string TranslatoinDescription { get; set; }
+
+ public ConcurrentDictionary TranslationResults { get; private set; }
+
+ private ActionBlock createTranslationActionBlock;
+
+ private ActionBlock<(Guid translationId, IReadOnlyDictionary additionalHeaders)> waitTranslationExecutionActionBlock;
+
+ public VideoTranslationPool(DeploymentEnvironment environment, string subKey)
+ : this()
+ {
+ this.FileClient = new VideoFileClient(environment, subKey);
+ this.TranslationClient = new VideoTranslationClient(environment, subKey);
+ }
+
+ public void Post(VideoTranslationPoolInputArgs args)
+ {
+ this.createTranslationActionBlock.Post(args);
+ }
+
+ public async Task WaitTranslationExecutionCompletion()
+ {
+ Console.WriteLine("Before this.waitTranslationExecutionActionBlock.Complete()");
+
+ this.waitTranslationExecutionActionBlock.Complete();
+
+ Console.WriteLine("Before await waitTranslationExecutionActionBlock.Completion");
+
+ await this.waitTranslationExecutionActionBlock.Completion.ConfigureAwait(false);
+ Console.WriteLine("After await waitTranslationExecutionActionBlock.Completion");
+ }
+
+ public async Task CompleteAndWaitTranslationCreationCompletion()
+ {
+ this.createTranslationActionBlock.Complete();
+ await this.createTranslationActionBlock.Completion.ConfigureAwait(false);
+ }
+}
diff --git a/VideoDubbing/CSharp/APIClientTool/nuget.config b/VideoDubbing/CSharp/APIClientTool/nuget.config
new file mode 100644
index 00000000..8c61ab3d
--- /dev/null
+++ b/VideoDubbing/CSharp/APIClientTool/nuget.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VideoDubbing/readme.md b/VideoDubbing/readme.md
new file mode 100644
index 00000000..46f6a04a
--- /dev/null
+++ b/VideoDubbing/readme.md
@@ -0,0 +1,19 @@
+# Video Dubbing
+
+Video dubbing client tool and API sample code
+
+# Solution:
+ [VideoTranslationApiSampleCode.sln](CSharp\APIClientTool\VideoTranslationApiSampleCode\VideoTranslationApiSampleCode.sln)
+
+
+# API sample:
+
+## Video file API:
+ Metadata API: [VideoFileClient.cs](CSharp\APIClientTool\VideoTranslationApiSampleCode\VideoTranslationLib\VideoTranslationMetadataClient.cs)
+
+ Video file API: [VideoFileClient.cs](CSharp\APIClientTool\VideoTranslationApiSampleCode\VideoTranslationLib\VideoFileClient.cs)
+
+ Translation API: [VideoTranslationClient.cs](CSharp\APIClientTool\VideoTranslationApiSampleCode\VideoTranslationLib\VideoTranslationClient.cs)
+
+ Target locale API: [TargetLocaleClient.cs](CSharp\APIClientTool\VideoTranslationApiSampleCode\VideoTranslationLib\TargetLocaleClient.cs)
+