Browse Source

no photo version

ming 3 years ago
parent
commit
dcaa873085
41 changed files with 2735 additions and 9 deletions
  1. BIN
      .DS_Store
  2. BIN
      OpenshotService/.DS_Store
  3. 405 0
      OpenshotService/autosub/__init__-0.4.0.py
  4. 434 0
      OpenshotService/autosub/__init__.py
  5. BIN
      OpenshotService/autosub/__pycache__/__init__.cpython-37.pyc
  6. BIN
      OpenshotService/autosub/__pycache__/constants.cpython-37.pyc
  7. BIN
      OpenshotService/autosub/__pycache__/formatters.cpython-37.pyc
  8. 118 0
      OpenshotService/autosub/constants.py
  9. 66 0
      OpenshotService/autosub/formatters.py
  10. 181 3
      OpenshotService/openshot_video_generator.py
  11. BIN
      OpenshotService/pytranscriber/.DS_Store
  12. 0 0
      OpenshotService/pytranscriber/control/__init__.py
  13. BIN
      OpenshotService/pytranscriber/control/__pycache__/__init__.cpython-37.pyc
  14. BIN
      OpenshotService/pytranscriber/control/__pycache__/ctr_autosub.cpython-37.pyc
  15. BIN
      OpenshotService/pytranscriber/control/__pycache__/ctr_main.cpython-37.pyc
  16. BIN
      OpenshotService/pytranscriber/control/__pycache__/thread_cancel_autosub.cpython-37.pyc
  17. BIN
      OpenshotService/pytranscriber/control/__pycache__/thread_exec_autosub.cpython-37.pyc
  18. 145 0
      OpenshotService/pytranscriber/control/ctr_autosub.py
  19. 413 0
      OpenshotService/pytranscriber/control/ctr_main.py
  20. 14 0
      OpenshotService/pytranscriber/control/thread_cancel_autosub.py
  21. 120 0
      OpenshotService/pytranscriber/control/thread_exec_autosub.py
  22. 0 0
      OpenshotService/pytranscriber/gui/__init__.py
  23. BIN
      OpenshotService/pytranscriber/gui/__pycache__/__init__.cpython-37.pyc
  24. BIN
      OpenshotService/pytranscriber/gui/__pycache__/gui.cpython-37.pyc
  25. 120 0
      OpenshotService/pytranscriber/gui/gui.py
  26. 266 0
      OpenshotService/pytranscriber/gui/gui.ui
  27. 0 0
      OpenshotService/pytranscriber/model/__init__.py
  28. BIN
      OpenshotService/pytranscriber/model/__pycache__/__init__.cpython-37.pyc
  29. BIN
      OpenshotService/pytranscriber/model/__pycache__/param_autosub.cpython-37.pyc
  30. 22 0
      OpenshotService/pytranscriber/model/param_autosub.py
  31. 0 0
      OpenshotService/pytranscriber/util/__init__.py
  32. BIN
      OpenshotService/pytranscriber/util/__pycache__/__init__.cpython-37.pyc
  33. BIN
      OpenshotService/pytranscriber/util/__pycache__/srtparser.cpython-37.pyc
  34. BIN
      OpenshotService/pytranscriber/util/__pycache__/util.cpython-37.pyc
  35. 49 0
      OpenshotService/pytranscriber/util/srtparser.py
  36. 44 0
      OpenshotService/pytranscriber/util/util.py
  37. BIN
      api/.DS_Store
  38. 85 0
      api/main.py
  39. 48 0
      api/static/script_util.js
  40. 170 0
      api/templates/make_video_long.html
  41. 35 6
      subGenerator/ProcessSub.py

BIN
.DS_Store


BIN
OpenshotService/.DS_Store


+ 405 - 0
OpenshotService/autosub/__init__-0.4.0.py

@@ -0,0 +1,405 @@
+"""
+Defines autosub's main functionality.
+"""
+
+#!/usr/bin/env python
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import argparse
+import audioop
+import json
+import math
+import multiprocessing
+import os
+import subprocess
+import sys
+import tempfile
+import wave
+
+import requests
+from googleapiclient.discovery import build
+from progressbar import ProgressBar, Percentage, Bar, ETA
+
+from autosub.constants import (
+    LANGUAGE_CODES, GOOGLE_SPEECH_API_KEY, GOOGLE_SPEECH_API_URL,
+)
+from autosub.formatters import FORMATTERS
+
+DEFAULT_SUBTITLE_FORMAT = 'srt'
+DEFAULT_CONCURRENCY = 10
+DEFAULT_SRC_LANGUAGE = 'en'
+DEFAULT_DST_LANGUAGE = 'en'
+
+
+def percentile(arr, percent):
+    """
+    Calculate the given percentile of arr.
+    """
+    arr = sorted(arr)
+    index = (len(arr) - 1) * percent
+    floor = math.floor(index)
+    ceil = math.ceil(index)
+    if floor == ceil:
+        return arr[int(index)]
+    low_value = arr[int(floor)] * (ceil - index)
+    high_value = arr[int(ceil)] * (index - floor)
+    return low_value + high_value
+
+
+class FLACConverter(object): # pylint: disable=too-few-public-methods
+    """
+    Class for converting a region of an input audio or video file into a FLAC audio file
+    """
+    def __init__(self, source_path, include_before=0.25, include_after=0.25):
+        self.source_path = source_path
+        self.include_before = include_before
+        self.include_after = include_after
+
+    def __call__(self, region):
+        try:
+            start, end = region
+            start = max(0, start - self.include_before)
+            end += self.include_after
+            temp = tempfile.NamedTemporaryFile(suffix='.flac')
+            command = ["ffmpeg", "-ss", str(start), "-t", str(end - start),
+                       "-y", "-i", self.source_path,
+                       "-loglevel", "error", temp.name]
+            use_shell = True if os.name == "nt" else False
+            subprocess.check_output(command, stdin=open(os.devnull), shell=use_shell)
+            return temp.read()
+
+        except KeyboardInterrupt:
+            return None
+
+
+class SpeechRecognizer(object): # pylint: disable=too-few-public-methods
+    """
+    Class for performing speech-to-text for an input FLAC file.
+    """
+    def __init__(self, language="en", rate=44100, retries=3, api_key=GOOGLE_SPEECH_API_KEY):
+        self.language = language
+        self.rate = rate
+        self.api_key = api_key
+        self.retries = retries
+
+    def __call__(self, data):
+        try:
+            for _ in range(self.retries):
+                url = GOOGLE_SPEECH_API_URL.format(lang=self.language, key=self.api_key)
+                headers = {"Content-Type": "audio/x-flac; rate=%d" % self.rate}
+
+                try:
+                    resp = requests.post(url, data=data, headers=headers)
+                except requests.exceptions.ConnectionError:
+                    continue
+
+                for line in resp.content.decode('utf-8').split("\n"):
+                    try:
+                        line = json.loads(line)
+                        line = line['result'][0]['alternative'][0]['transcript']
+                        return line[:1].upper() + line[1:]
+                    except IndexError:
+                        # no result
+                        continue
+
+        except KeyboardInterrupt:
+            return None
+
+
+class Translator(object): # pylint: disable=too-few-public-methods
+    """
+    Class for translating a sentence from a one language to another.
+    """
+    def __init__(self, language, api_key, src, dst):
+        self.language = language
+        self.api_key = api_key
+        self.service = build('translate', 'v2',
+                             developerKey=self.api_key)
+        self.src = src
+        self.dst = dst
+
+    def __call__(self, sentence):
+        try:
+            if not sentence:
+                return None
+
+            result = self.service.translations().list( # pylint: disable=no-member
+                source=self.src,
+                target=self.dst,
+                q=[sentence]
+            ).execute()
+
+            if 'translations' in result and result['translations'] and \
+                'translatedText' in result['translations'][0]:
+                return result['translations'][0]['translatedText']
+
+            return None
+
+        except KeyboardInterrupt:
+            return None
+
+
+def which(program):
+    """
+    Return the path for a given executable.
+    """
+    def is_exe(file_path):
+        """
+        Checks whether a file is executable.
+        """
+        return os.path.isfile(file_path) and os.access(file_path, os.X_OK)
+
+    fpath, _ = os.path.split(program)
+    if fpath:
+        if is_exe(program):
+            return program
+    else:
+        for path in os.environ["PATH"].split(os.pathsep):
+            path = path.strip('"')
+            exe_file = os.path.join(path, program)
+            if is_exe(exe_file):
+                return exe_file
+    return None
+
+
+def extract_audio(filename, channels=1, rate=16000):
+    """
+    Extract audio from an input file to a temporary WAV file.
+    """
+    temp = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
+    if not os.path.isfile(filename):
+        print("The given file does not exist: {}".format(filename))
+        raise Exception("Invalid filepath: {}".format(filename))
+    if not which("ffmpeg"):
+        print("ffmpeg: Executable not found on machine.")
+        raise Exception("Dependency not found: ffmpeg")
+    command = ["ffmpeg", "-y", "-i", filename,
+               "-ac", str(channels), "-ar", str(rate),
+               "-loglevel", "error", temp.name]
+    use_shell = True if os.name == "nt" else False
+    subprocess.check_output(command, stdin=open(os.devnull), shell=use_shell)
+    return temp.name, rate
+
+
+def find_speech_regions(filename, frame_width=4096, min_region_size=0.5, max_region_size=6): # pylint: disable=too-many-locals
+    """
+    Perform voice activity detection on a given audio file.
+    """
+    reader = wave.open(filename)
+    sample_width = reader.getsampwidth()
+    rate = reader.getframerate()
+    n_channels = reader.getnchannels()
+    chunk_duration = float(frame_width) / rate
+
+    n_chunks = int(math.ceil(reader.getnframes()*1.0 / frame_width))
+    energies = []
+
+    for _ in range(n_chunks):
+        chunk = reader.readframes(frame_width)
+        energies.append(audioop.rms(chunk, sample_width * n_channels))
+
+    threshold = percentile(energies, 0.2)
+
+    elapsed_time = 0
+
+    regions = []
+    region_start = None
+
+    for energy in energies:
+        is_silence = energy <= threshold
+        max_exceeded = region_start and elapsed_time - region_start >= max_region_size
+
+        if (max_exceeded or is_silence) and region_start:
+            if elapsed_time - region_start >= min_region_size:
+                regions.append((region_start, elapsed_time))
+                region_start = None
+
+        elif (not region_start) and (not is_silence):
+            region_start = elapsed_time
+        elapsed_time += chunk_duration
+    return regions
+
+
+def generate_subtitles( # pylint: disable=too-many-locals,too-many-arguments
+        source_path,
+        output=None,
+        concurrency=DEFAULT_CONCURRENCY,
+        src_language=DEFAULT_SRC_LANGUAGE,
+        dst_language=DEFAULT_DST_LANGUAGE,
+        subtitle_file_format=DEFAULT_SUBTITLE_FORMAT,
+        api_key=None,
+    ):
+    """
+    Given an input audio/video file, generate subtitles in the specified language and format.
+    """
+    audio_filename, audio_rate = extract_audio(source_path)
+
+    regions = find_speech_regions(audio_filename)
+
+    pool = multiprocessing.Pool(concurrency)
+    converter = FLACConverter(source_path=audio_filename)
+    recognizer = SpeechRecognizer(language=src_language, rate=audio_rate,
+                                  api_key=GOOGLE_SPEECH_API_KEY)
+
+    transcripts = []
+    if regions:
+        try:
+            widgets = ["Converting speech regions to FLAC files: ", Percentage(), ' ', Bar(), ' ',
+                       ETA()]
+            pbar = ProgressBar(widgets=widgets, maxval=len(regions)).start()
+            extracted_regions = []
+            for i, extracted_region in enumerate(pool.imap(converter, regions)):
+                extracted_regions.append(extracted_region)
+                pbar.update(i)
+            pbar.finish()
+
+            widgets = ["Performing speech recognition: ", Percentage(), ' ', Bar(), ' ', ETA()]
+            pbar = ProgressBar(widgets=widgets, maxval=len(regions)).start()
+
+            for i, transcript in enumerate(pool.imap(recognizer, extracted_regions)):
+                transcripts.append(transcript)
+                pbar.update(i)
+            pbar.finish()
+
+            if src_language.split("-")[0] != dst_language.split("-")[0]:
+                if api_key:
+                    google_translate_api_key = api_key
+                    translator = Translator(dst_language, google_translate_api_key,
+                                            dst=dst_language,
+                                            src=src_language)
+                    prompt = "Translating from {0} to {1}: ".format(src_language, dst_language)
+                    widgets = [prompt, Percentage(), ' ', Bar(), ' ', ETA()]
+                    pbar = ProgressBar(widgets=widgets, maxval=len(regions)).start()
+                    translated_transcripts = []
+                    for i, transcript in enumerate(pool.imap(translator, transcripts)):
+                        translated_transcripts.append(transcript)
+                        pbar.update(i)
+                    pbar.finish()
+                    transcripts = translated_transcripts
+                else:
+                    print(
+                        "Error: Subtitle translation requires specified Google Translate API key. "
+                        "See --help for further information."
+                    )
+                    return 1
+
+        except KeyboardInterrupt:
+            pbar.finish()
+            pool.terminate()
+            pool.join()
+            print("Cancelling transcription")
+            raise
+
+    timed_subtitles = [(r, t) for r, t in zip(regions, transcripts) if t]
+    formatter = FORMATTERS.get(subtitle_file_format)
+    formatted_subtitles = formatter(timed_subtitles)
+
+    dest = output
+
+    if not dest:
+        base = os.path.splitext(source_path)[0]
+        dest = "{base}.{format}".format(base=base, format=subtitle_file_format)
+
+    with open(dest, 'wb') as output_file:
+        output_file.write(formatted_subtitles.encode("utf-8"))
+
+    os.remove(audio_filename)
+
+    return dest
+
+
+def validate(args):
+    """
+    Check that the CLI arguments passed to autosub are valid.
+    """
+    if args.format not in FORMATTERS:
+        print(
+            "Subtitle format not supported. "
+            "Run with --list-formats to see all supported formats."
+        )
+        return False
+
+    if args.src_language not in LANGUAGE_CODES.keys():
+        print(
+            "Source language not supported. "
+            "Run with --list-languages to see all supported languages."
+        )
+        return False
+
+    if args.dst_language not in LANGUAGE_CODES.keys():
+        print(
+            "Destination language not supported. "
+            "Run with --list-languages to see all supported languages."
+        )
+        return False
+
+    if not args.source_path:
+        print("Error: You need to specify a source path.")
+        return False
+
+    return True
+
+
+def main():
+    """
+    Run autosub as a command-line program.
+    """
+    parser = argparse.ArgumentParser()
+    parser.add_argument('source_path', help="Path to the video or audio file to subtitle",
+                        nargs='?')
+    parser.add_argument('-C', '--concurrency', help="Number of concurrent API requests to make",
+                        type=int, default=DEFAULT_CONCURRENCY)
+    parser.add_argument('-o', '--output',
+                        help="Output path for subtitles (by default, subtitles are saved in \
+                        the same directory and name as the source path)")
+    parser.add_argument('-F', '--format', help="Destination subtitle format",
+                        default=DEFAULT_SUBTITLE_FORMAT)
+    parser.add_argument('-S', '--src-language', help="Language spoken in source file",
+                        default=DEFAULT_SRC_LANGUAGE)
+    parser.add_argument('-D', '--dst-language', help="Desired language for the subtitles",
+                        default=DEFAULT_DST_LANGUAGE)
+    parser.add_argument('-K', '--api-key',
+                        help="The Google Translate API key to be used. \
+                        (Required for subtitle translation)")
+    parser.add_argument('--list-formats', help="List all available subtitle formats",
+                        action='store_true')
+    parser.add_argument('--list-languages', help="List all available source/destination languages",
+                        action='store_true')
+
+    args = parser.parse_args()
+
+    if args.list_formats:
+        print("List of formats:")
+        for subtitle_format in FORMATTERS:
+            print("{format}".format(format=subtitle_format))
+        return 0
+
+    if args.list_languages:
+        print("List of all languages:")
+        for code, language in sorted(LANGUAGE_CODES.items()):
+            print("{code}\t{language}".format(code=code, language=language))
+        return 0
+
+    if not validate(args):
+        return 1
+
+    try:
+        subtitle_file_path = generate_subtitles(
+            source_path=args.source_path,
+            concurrency=args.concurrency,
+            src_language=args.src_language,
+            dst_language=args.dst_language,
+            api_key=args.api_key,
+            subtitle_file_format=args.format,
+            output=args.output,
+        )
+        print("Subtitles file created at {}".format(subtitle_file_path))
+    except KeyboardInterrupt:
+        return 1
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 434 - 0
OpenshotService/autosub/__init__.py

@@ -0,0 +1,434 @@
+"""
+Defines autosub's main functionality.
+"""
+
+#!/usr/bin/env python
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import argparse
+import audioop
+import math
+import multiprocessing
+import os
+from json import JSONDecodeError
+import subprocess
+import sys
+import tempfile
+import wave
+
+import json
+import requests
+try:
+    from json.decoder import JSONDecodeError
+except ImportError:
+    JSONDecodeError = ValueError
+
+from googleapiclient.discovery import build
+from progressbar import ProgressBar, Percentage, Bar, ETA
+
+from autosub.constants import (
+    LANGUAGE_CODES, GOOGLE_SPEECH_API_KEY, GOOGLE_SPEECH_API_URL,
+)
+from autosub.formatters import FORMATTERS
+
+DEFAULT_SUBTITLE_FORMAT = 'srt'
+DEFAULT_CONCURRENCY = 10
+DEFAULT_SRC_LANGUAGE = 'en'
+DEFAULT_DST_LANGUAGE = 'en'
+
+
+def percentile(arr, percent):
+    """
+    Calculate the given percentile of arr.
+    """
+    arr = sorted(arr)
+    index = (len(arr) - 1) * percent
+    floor = math.floor(index)
+    ceil = math.ceil(index)
+    if floor == ceil:
+        return arr[int(index)]
+    low_value = arr[int(floor)] * (ceil - index)
+    high_value = arr[int(ceil)] * (index - floor)
+    return low_value + high_value
+
+
+class FLACConverter(object): # pylint: disable=too-few-public-methods
+    """
+    Class for converting a region of an input audio or video file into a FLAC audio file
+    """
+    def __init__(self, source_path, include_before=0.25, include_after=0.25):
+        self.source_path = source_path
+        self.include_before = include_before
+        self.include_after = include_after
+
+    def __call__(self, region):
+        try:
+            start, end = region
+            start = max(0, start - self.include_before)
+            end += self.include_after
+            #delete=False necessary for running on Windows
+            temp = tempfile.NamedTemporaryFile(suffix='.flac', delete=False)
+            program_ffmpeg = which("ffmpeg")
+            command = [str(program_ffmpeg), "-ss", str(start), "-t", str(end - start),
+                       "-y", "-i", self.source_path,
+                       "-loglevel", "error", temp.name]
+            use_shell = True if os.name == "nt" else False
+            subprocess.check_output(command, stdin=open(os.devnull), shell=use_shell)
+            read_data = temp.read()
+            temp.close()
+            os.unlink(temp.name)
+            return read_data
+
+        except KeyboardInterrupt:
+            return None
+
+
+class SpeechRecognizer(object): # pylint: disable=too-few-public-methods
+    """
+    Class for performing speech-to-text for an input FLAC file.
+    """
+    def __init__(self, language="en", rate=44100, retries=3, api_key=GOOGLE_SPEECH_API_KEY):
+        self.language = language
+        self.rate = rate
+        self.api_key = api_key
+        self.retries = retries
+
+    def __call__(self, data):
+        try:
+            for _ in range(self.retries):
+                url = GOOGLE_SPEECH_API_URL.format(lang=self.language, key=self.api_key)
+                headers = {"Content-Type": "audio/x-flac; rate=%d" % self.rate}
+
+                try:
+                    resp = requests.post(url, data=data, headers=headers)
+                except requests.exceptions.ConnectionError:
+                    continue
+
+                for line in resp.content.decode('utf-8').split("\n"):
+                    try:
+                        line = json.loads(line)
+                        line = line['result'][0]['alternative'][0]['transcript']
+                        return line[:1].upper() + line[1:]
+                    except IndexError:
+                        # no result
+                        continue
+                    except JSONDecodeError:
+                        continue
+
+        except KeyboardInterrupt:
+            return None
+
+
+class Translator(object): # pylint: disable=too-few-public-methods
+    """
+    Class for translating a sentence from a one language to another.
+    """
+    def __init__(self, language, api_key, src, dst):
+        self.language = language
+        self.api_key = api_key
+        self.service = build('translate', 'v2',
+                             developerKey=self.api_key)
+        self.src = src
+        self.dst = dst
+
+    def __call__(self, sentence):
+        try:
+            if not sentence:
+                return None
+
+            result = self.service.translations().list( # pylint: disable=no-member
+                source=self.src,
+                target=self.dst,
+                q=[sentence]
+            ).execute()
+
+            if 'translations' in result and result['translations'] and \
+                'translatedText' in result['translations'][0]:
+                return result['translations'][0]['translatedText']
+
+            return None
+
+        except KeyboardInterrupt:
+            return None
+
+
+def which(program):
+    """
+    Return the path for a given executable.
+    """
+    def is_exe(file_path):
+        """
+        Checks whether a file is executable.
+        """
+        return os.path.isfile(file_path) and os.access(file_path, os.X_OK)
+    #necessary to run on Windows
+    if os.name == "nt":
+        program += ".exe"
+    fpath, _ = os.path.split(program)
+    if fpath:
+        if is_exe(program):
+            return program
+    else:
+        #looks for file in the script execution folder before checking on system path
+        current_dir = os.getcwd()
+        local_program = os.path.join(current_dir, program)
+        if is_exe(local_program):
+            return local_program
+        else:
+            for path in os.environ["PATH"].split(os.pathsep):
+                path = path.strip('"')
+                exe_file = os.path.join(path, program)
+                if is_exe(exe_file):
+                    return exe_file
+    return None
+
+
+def extract_audio(filename, channels=1, rate=16000):
+    """
+    Extract audio from an input file to a temporary WAV file.
+    """
+    temp = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
+    if not os.path.isfile(filename):
+        print("The given file does not exist: {}".format(filename))
+        raise Exception("Invalid filepath: {}".format(filename))
+    program_ffmpeg = which("ffmpeg")
+    if not program_ffmpeg:
+        print("ffmpeg: Executable not found on machine.")
+        raise Exception("Dependency not found: ffmpeg")
+    command = [str(program_ffmpeg), "-y", "-i", filename,
+               "-ac", str(channels), "-ar", str(rate),
+               "-loglevel", "error", temp.name]
+    use_shell = True if os.name == "nt" else False
+    subprocess.check_output(command, stdin=open(os.devnull), shell=use_shell)
+    return temp.name, rate
+
+
+def find_speech_regions(filename, frame_width=4096, min_region_size=0.5, max_region_size=6): # pylint: disable=too-many-locals
+    """
+    Perform voice activity detection on a given audio file.
+    """
+    reader = wave.open(filename)
+    sample_width = reader.getsampwidth()
+    rate = reader.getframerate()
+    n_channels = reader.getnchannels()
+    chunk_duration = float(frame_width) / rate
+
+    n_chunks = int(math.ceil(reader.getnframes()*1.0 / frame_width))
+    energies = []
+
+    for _ in range(n_chunks):
+        chunk = reader.readframes(frame_width)
+        energies.append(audioop.rms(chunk, sample_width * n_channels))
+
+    threshold = percentile(energies, 0.2)
+
+    elapsed_time = 0
+
+    regions = []
+    region_start = None
+
+    for energy in energies:
+        is_silence = energy <= threshold
+        max_exceeded = region_start and elapsed_time - region_start >= max_region_size
+
+        if (max_exceeded or is_silence) and region_start:
+            if elapsed_time - region_start >= min_region_size:
+                regions.append((region_start, elapsed_time))
+                region_start = None
+
+        elif (not region_start) and (not is_silence):
+            region_start = elapsed_time
+        elapsed_time += chunk_duration
+    return regions
+
+
+def generate_subtitles( # pylint: disable=too-many-locals,too-many-arguments
+        source_path,
+        output=None,
+        concurrency=DEFAULT_CONCURRENCY,
+        src_language=DEFAULT_SRC_LANGUAGE,
+        dst_language=DEFAULT_DST_LANGUAGE,
+        subtitle_file_format=DEFAULT_SUBTITLE_FORMAT,
+        api_key=None,
+    ):
+    """
+    Given an input audio/video file, generate subtitles in the specified language and format.
+    """
+
+    if os.name != "nt" and "Darwin" in os.uname():
+        #the default unix fork method does not work on Mac OS
+        #need to use forkserver
+        if 'forkserver' != multiprocessing.get_start_method(allow_none=True):
+            multiprocessing.set_start_method('forkserver')
+
+    audio_filename, audio_rate = extract_audio(source_path)
+
+    regions = find_speech_regions(audio_filename)
+
+    pool = multiprocessing.Pool(concurrency)
+    converter = FLACConverter(source_path=audio_filename)
+    recognizer = SpeechRecognizer(language=src_language, rate=audio_rate,
+                                  api_key=GOOGLE_SPEECH_API_KEY)
+
+    transcripts = []
+    if regions:
+        try:
+            widgets = ["Converting speech regions to FLAC files: ", Percentage(), ' ', Bar(), ' ',
+                       ETA()]
+            pbar = ProgressBar(widgets=widgets, maxval=len(regions)).start()
+            extracted_regions = []
+            for i, extracted_region in enumerate(pool.imap(converter, regions)):
+                extracted_regions.append(extracted_region)
+                pbar.update(i)
+            pbar.finish()
+
+            widgets = ["Performing speech recognition: ", Percentage(), ' ', Bar(), ' ', ETA()]
+            pbar = ProgressBar(widgets=widgets, maxval=len(regions)).start()
+
+            for i, transcript in enumerate(pool.imap(recognizer, extracted_regions)):
+                transcripts.append(transcript)
+                pbar.update(i)
+            pbar.finish()
+
+            if src_language.split("-")[0] != dst_language.split("-")[0]:
+                if api_key:
+                    google_translate_api_key = api_key
+                    translator = Translator(dst_language, google_translate_api_key,
+                                            dst=dst_language,
+                                            src=src_language)
+                    prompt = "Translating from {0} to {1}: ".format(src_language, dst_language)
+                    widgets = [prompt, Percentage(), ' ', Bar(), ' ', ETA()]
+                    pbar = ProgressBar(widgets=widgets, maxval=len(regions)).start()
+                    translated_transcripts = []
+                    for i, transcript in enumerate(pool.imap(translator, transcripts)):
+                        translated_transcripts.append(transcript)
+                        pbar.update(i)
+                    pbar.finish()
+                    transcripts = translated_transcripts
+                else:
+                    print(
+                        "Error: Subtitle translation requires specified Google Translate API key. "
+                        "See --help for further information."
+                    )
+                    return 1
+
+        except KeyboardInterrupt:
+            pbar.finish()
+            pool.terminate()
+            pool.join()
+            print("Cancelling transcription")
+            raise
+
+    timed_subtitles = [(r, t) for r, t in zip(regions, transcripts) if t]
+    formatter = FORMATTERS.get(subtitle_file_format)
+    formatted_subtitles = formatter(timed_subtitles)
+
+    dest = output
+
+    if not dest:
+        base = os.path.splitext(source_path)[0]
+        dest = "{base}.{format}".format(base=base, format=subtitle_file_format)
+
+    with open(dest, 'wb') as output_file:
+        output_file.write(formatted_subtitles.encode("utf-8"))
+
+    os.remove(audio_filename)
+
+    return dest
+
+
+def validate(args):
+    """
+    Check that the CLI arguments passed to autosub are valid.
+    """
+    if args.format not in FORMATTERS:
+        print(
+            "Subtitle format not supported. "
+            "Run with --list-formats to see all supported formats."
+        )
+        return False
+
+    if args.src_language not in LANGUAGE_CODES.keys():
+        print(
+            "Source language not supported. "
+            "Run with --list-languages to see all supported languages."
+        )
+        return False
+
+    if args.dst_language not in LANGUAGE_CODES.keys():
+        print(
+            "Destination language not supported. "
+            "Run with --list-languages to see all supported languages."
+        )
+        return False
+
+    if not args.source_path:
+        print("Error: You need to specify a source path.")
+        return False
+
+    return True
+
+
+def main():
+    """
+    Run autosub as a command-line program.
+    """
+    parser = argparse.ArgumentParser()
+    parser.add_argument('source_path', help="Path to the video or audio file to subtitle",
+                        nargs='?')
+    parser.add_argument('-C', '--concurrency', help="Number of concurrent API requests to make",
+                        type=int, default=DEFAULT_CONCURRENCY)
+    parser.add_argument('-o', '--output',
+                        help="Output path for subtitles (by default, subtitles are saved in \
+                        the same directory and name as the source path)")
+    parser.add_argument('-F', '--format', help="Destination subtitle format",
+                        default=DEFAULT_SUBTITLE_FORMAT)
+    parser.add_argument('-S', '--src-language', help="Language spoken in source file",
+                        default=DEFAULT_SRC_LANGUAGE)
+    parser.add_argument('-D', '--dst-language', help="Desired language for the subtitles",
+                        default=DEFAULT_DST_LANGUAGE)
+    parser.add_argument('-K', '--api-key',
+                        help="The Google Translate API key to be used. \
+                        (Required for subtitle translation)")
+    parser.add_argument('--list-formats', help="List all available subtitle formats",
+                        action='store_true')
+    parser.add_argument('--list-languages', help="List all available source/destination languages",
+                        action='store_true')
+
+    args = parser.parse_args()
+
+    if args.list_formats:
+        print("List of formats:")
+        for subtitle_format in FORMATTERS:
+            print("{format}".format(format=subtitle_format))
+        return 0
+
+    if args.list_languages:
+        print("List of all languages:")
+        for code, language in sorted(LANGUAGE_CODES.items()):
+            print("{code}\t{language}".format(code=code, language=language))
+        return 0
+
+    if not validate(args):
+        return 1
+
+    try:
+        subtitle_file_path = generate_subtitles(
+            source_path=args.source_path,
+            concurrency=args.concurrency,
+            src_language=args.src_language,
+            dst_language=args.dst_language,
+            api_key=args.api_key,
+            subtitle_file_format=args.format,
+            output=args.output,
+        )
+        print("Subtitles file created at {}".format(subtitle_file_path))
+    except KeyboardInterrupt:
+        return 1
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())

BIN
OpenshotService/autosub/__pycache__/__init__.cpython-37.pyc


BIN
OpenshotService/autosub/__pycache__/constants.cpython-37.pyc


BIN
OpenshotService/autosub/__pycache__/formatters.cpython-37.pyc


+ 118 - 0
OpenshotService/autosub/constants.py

@@ -0,0 +1,118 @@
+"""
+Defines constants used by autosub.
+"""
+
+from __future__ import unicode_literals
+
+GOOGLE_SPEECH_API_KEY = "AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw"
+GOOGLE_SPEECH_API_URL = "http://www.google.com/speech-api/v2/recognize?client=chromium&lang={lang}&key={key}" # pylint: disable=line-too-long
+
+LANGUAGE_CODES = {
+    'af': 'Afrikaans',
+    'ar': 'Arabic',
+    'az': 'Azerbaijani',
+    'be': 'Belarusian',
+    'bg': 'Bulgarian',
+    'bn': 'Bengali',
+    'bs': 'Bosnian',
+    'ca': 'Catalan',
+    'ceb': 'Cebuano',
+    'cs': 'Czech',
+    'cy': 'Welsh',
+    'da': 'Danish',
+    'de': 'German',
+    'el': 'Greek',
+    'en-AU': 'English (Australia)',
+    'en-CA': 'English (Canada)',
+    'en-GB': 'English (United Kingdom)',
+    'en-IN': 'English (India)',
+    'en-IE': 'English (Ireland)',
+    'en-NZ': 'English (New Zealand)',
+    'en-PH': 'English (Philippines)',
+    'en-SG': 'English (Singapore)',
+    'en-US': 'English (United States)',
+    'eo': 'Esperanto',
+    'es-AR': 'Spanish (Argentina)',
+    'es-CL': 'Spanish (Chile)',
+    'es-ES': 'Spanish (Spain)',
+    'es-US': 'Spanish (United States)',
+    'es-MX': 'Spanish (Mexico)',
+    'es': 'Spanish',
+    'et': 'Estonian',
+    'eu': 'Basque',
+    'fa': 'Persian',
+    'fi': 'Finnish',
+    'fr': 'French',
+    'ga': 'Irish',
+    'gl': 'Galician',
+    'gu': 'Gujarati',
+    'ha': 'Hausa',
+    'hi': 'Hindi',
+    'hmn': 'Hmong',
+    'hr': 'Croatian',
+    'ht': 'Haitian Creole',
+    'hu': 'Hungarian',
+    'hy': 'Armenian',
+    'id': 'Indonesian',
+    'ig': 'Igbo',
+    'is': 'Icelandic',
+    'it': 'Italian',
+    'iw': 'Hebrew',
+    'ja': 'Japanese',
+    'jw': 'Javanese',
+    'ka': 'Georgian',
+    'kk': 'Kazakh',
+    'km': 'Khmer',
+    'kn': 'Kannada',
+    'ko': 'Korean',
+    'la': 'Latin',
+    'lo': 'Lao',
+    'lt': 'Lithuanian',
+    'lv': 'Latvian',
+    'mg': 'Malagasy',
+    'mi': 'Maori',
+    'mk': 'Macedonian',
+    'ml': 'Malayalam',
+    'mn': 'Mongolian',
+    'mr': 'Marathi',
+    'ms': 'Malay',
+    'mt': 'Maltese',
+    'my': 'Myanmar (Burmese)',
+    'ne': 'Nepali',
+    'nl': 'Dutch',
+    'no': 'Norwegian',
+    'ny': 'Chichewa',
+    'pa': 'Punjabi',
+    'pl': 'Polish',
+    'pt-BR': 'Portuguese (Brazil)',
+    'pt-PT': 'Portuguese (Portugal)',
+    'ro': 'Romanian',
+    'ru': 'Russian',
+    'si': 'Sinhala',
+    'sk': 'Slovak',
+    'sl': 'Slovenian',
+    'so': 'Somali',
+    'sq': 'Albanian',
+    'sr': 'Serbian',
+    'st': 'Sesotho',
+    'su': 'Sudanese',
+    'sv': 'Swedish',
+    'sw': 'Swahili',
+    'ta': 'Tamil',
+    'te': 'Telugu',
+    'tg': 'Tajik',
+    'th': 'Thai',
+    'tl': 'Filipino',
+    'tr': 'Turkish',
+    'uk': 'Ukrainian',
+    'ur': 'Urdu',
+    'uz': 'Uzbek',
+    'vi': 'Vietnamese',
+    'yi': 'Yiddish',
+    'yo': 'Yoruba',
+    'yue-Hant-HK': 'Cantonese, (Traditional HK)',
+    'zh': 'Chinese (Simplified, China)',
+    'zh-HK': 'Chinese (Simplified, Hong Kong)',
+    'zh-TW': 'Chinese (Traditional, Taiwan)',
+    'zu': 'Zulu',
+}

+ 66 - 0
OpenshotService/autosub/formatters.py

@@ -0,0 +1,66 @@
+"""
+Defines subtitle formatters used by autosub.
+"""
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import json
+
+import pysrt
+import six
+
+
+def srt_formatter(subtitles, padding_before=0, padding_after=0):
+    """
+    Serialize a list of subtitles according to the SRT format, with optional time padding.
+    """
+    sub_rip_file = pysrt.SubRipFile()
+    for i, ((start, end), text) in enumerate(subtitles, start=1):
+        item = pysrt.SubRipItem()
+        item.index = i
+        item.text = six.text_type(text)
+        item.start.seconds = max(0, start - padding_before)
+        item.end.seconds = end + padding_after
+        sub_rip_file.append(item)
+    return '\n'.join(six.text_type(item) for item in sub_rip_file)
+
+
+def vtt_formatter(subtitles, padding_before=0, padding_after=0):
+    """
+    Serialize a list of subtitles according to the VTT format, with optional time padding.
+    """
+    text = srt_formatter(subtitles, padding_before, padding_after)
+    text = 'WEBVTT\n\n' + text.replace(',', '.')
+    return text
+
+
+def json_formatter(subtitles):
+    """
+    Serialize a list of subtitles as a JSON blob.
+    """
+    subtitle_dicts = [
+        {
+            'start': start,
+            'end': end,
+            'content': text,
+        }
+        for ((start, end), text)
+        in subtitles
+    ]
+    return json.dumps(subtitle_dicts)
+
+
+def raw_formatter(subtitles):
+    """
+    Serialize a list of subtitles as a newline-delimited string.
+    """
+    return ' '.join(text for (_rng, text) in subtitles)
+
+
+FORMATTERS = {
+    'srt': srt_formatter,
+    'vtt': vtt_formatter,
+    'json': json_formatter,
+    'raw': raw_formatter,
+}

+ 181 - 3
OpenshotService/openshot_video_generator.py

@@ -20,6 +20,13 @@ import dataset
 from datetime import datetime
 from gtts import gTTS
 import ffmpy
+from difflib import SequenceMatcher
+import difflib
+from autosub import DEFAULT_CONCURRENCY
+from autosub import DEFAULT_SUBTITLE_FORMAT
+from pytranscriber.control.ctr_main import Ctr_Main
+from pytranscriber.control.ctr_autosub import Ctr_Autosub
+import multiprocessing
 
 dir_sound = 'mp3_track/'
 dir_photo = 'photo/'
@@ -69,6 +76,8 @@ def video_photo_clip(vid=None,layer=None, position=None, end=None
         clip.has_audio=openshot.Keyframe(0)
     return clip
 
+def listener_progress(string, percent):
+    True
 
 def myunichchar(unicode_char):
         mb_string = unicode_char.encode('big5')
@@ -226,6 +235,29 @@ def call_anchor(fileName,avatar):
     fr.close()
     fw.close()
 
+def parse_script(file_path):
+    with open(file_path, 'r') as f:
+        lines = [line.strip() for line in f]
+    dict_list = []
+    
+    for idx in range(int((len(lines)+1)/4)):
+        script={}
+        script['index'] = lines[idx * 4]
+        time_raw = lines[idx * 4 + 1]
+        script['content'] = lines[idx * 4 + 2]
+        start = time_raw.split(' --> ')[0].split(':')
+        stop = time_raw.split(' --> ')[1].split(':')
+        start[2] = start[2].replace(',','.')
+        stop[2] = stop[2].replace(',','.')
+        start_sec = float(start[0])*3600 + float(start[1])*60 + float(start[2])
+        stop_sec = float(stop[0])*3600 + float(stop[1])*60 + float(stop[2])
+        duration = start_sec-stop_sec
+        script['start'] = start_sec
+        script['stop'] = stop_sec
+        script['duration'] = abs(duration)
+        dict_list.append(script)
+    return dict_list
+
 def trim_punctuation(s):
     pat_block = u'[^\u4e00-\u9fff0-9a-zA-Z]+';
     pattern = u'([0-9]+{0}[0-9]+)|{0}'.format(pat_block)
@@ -240,6 +272,12 @@ def split_by_pun(s):
     res = list(splitter(s))
     return res
 
+def generate_subtitle_image_from_dict(name_hash, sub_dict):
+    for script in sub_dict:
+        sv_path = dir_subtitle + name_hash + '/' + str(script['index'])+'.png'
+        sub = script['content']
+        txt2image(sub,sv_path)
+
 def generate_subtitle_image(name_hash,text_content):
     img_list = [None]*len(text_content)
     for idx in range(len(text_content)):
@@ -261,6 +299,146 @@ def generate_subtitle_image_ENG(name_hash,text_content):
         img_list[idx] = sv_path
     return img_list
 
+def video_writer_init(path):
+    w = openshot.FFmpegWriter(path)
+    w.SetAudioOptions(True, "aac", 44100, 2, openshot.LAYOUT_STEREO, 3000000)
+    w.SetVideoOptions(True, "libx264", openshot.Fraction(30000, 1000), 1280, 720,
+        openshot.Fraction(1, 1), False, False, 3000000)
+    return w
+
+
+
+def video_gen(name_hash,name,text_content, image_urls,multiLang,avatar):
+    file_prepare(name, name_hash, text_content,image_urls,multiLang)
+    
+    for fname in range(len(text_content)):
+        call_anchor(name_hash+"/"+str(fname),avatar)
+    print('called............................................')
+    ck=cKey(0,254,0,270)
+    ck_anchor=cKey(0,255,1,320)
+    t = openshot.Timeline(1280, 720, openshot.Fraction(30000, 1000), 44100, 2, openshot.LAYOUT_STEREO)
+    t.Open()
+    main_timer = 0
+    LOGO_OP = openshot.FFmpegReader(dir_video+"LOGO_OP_4.mp4")
+    LOGO_OP.Open()         # Open the reader
+    head_duration = LOGO_OP.info.duration
+    LOGO_OP_clip = video_photo_clip(vid=LOGO_OP,layer=4,position=0,end=head_duration
+                    ,location_y=-0.03,scale_x=0.8,scale_y=0.704)
+    t.AddClip(LOGO_OP_clip)
+    bg_head = openshot.FFmpegReader(dir_video+"complete_head_aispokesgirl.mp4")
+    bg_head.Open()
+    bg_head_clip = video_photo_clip(vid=bg_head,layer=2,position=0,end=LOGO_OP.info.duration,ck=ck)
+    t.AddClip(bg_head_clip)
+    main_timer += head_duration
+    bg_head.Close()
+    LOGO_OP.Close()
+    
+    anchor = openshot.FFmpegReader(dir_anchor+name_hash+"/0.mp4")
+    anchor.Open()
+    #anchor_clip = video_photo_clip(vid=anchor,layer=4,scale_x=0.65,scale_y=0.65,
+    #        location_x=0.35,location_y=0.25,position=main_timer, end=anchor.info.duration,ck=ck_anchor,audio=False)
+    #t.AddClip(anchor_clip)
+
+    speech = openshot.FFmpegReader(dir_sound+name_hash+"/0.mp3")
+    speech.Open()
+    speech_clip = openshot.Clip(speech)
+    speech_clip.Position(main_timer)
+    speech_clip.End(anchor.info.duration)
+    t.AddClip(speech_clip)
+    main_timer += anchor.info.duration
+    anchor.Close()
+    speech.Close()
+    
+    LOGO_ED = openshot.FFmpegReader(dir_video+"LOGO_ED.avi")
+    LOGO_ED.Open()
+    LOGO_ED_clip = video_photo_clip(vid=LOGO_ED,layer=4,position=main_timer,end=LOGO_ED.info.duration
+                    ,location_x=0.005,location_y=-0.031, scale_x=0.8,scale_y=0.6825)
+    t.AddClip(LOGO_ED_clip)
+    main_timer += LOGO_ED.info.duration
+    LOGO_ED.Close()
+    
+    bg = openshot.FFmpegReader(dir_video+"complete_double_aispokesgirl.mp4")
+    bg.Open()
+    bg_times = math.floor(main_timer/bg.info.duration)
+    left_time = (main_timer) % bg.info.duration
+    bg_clip_list = [None] * bg_times
+    bg_list = [None] * bg_times
+    bg.Close()
+    bg_timer = head_duration
+    for idx in range(bg_times):
+        bg_list[idx] = openshot.FFmpegReader(dir_video+"complete_double_aispokesgirl.mp4")
+        bg_list[idx].Open()
+        bg_clip_list[idx] = video_photo_clip(bg_list[idx],layer=2,position=bg_timer,end=bg_list[idx].info.duration,ck=ck)
+        t.AddClip(bg_clip_list[idx])
+        bg_timer += bg_list[idx].info.duration
+        bg_list[idx].Close()
+    bg_left = openshot.FFmpegReader(dir_video+"complete_double_aispokesgirl.mp4").Open()
+    bg_left_clip = video_photo_clip(bg_left,layer=2,position=bg_timer,end=left_time,ck=ck)
+    t.AddClip(bg_left_clip)
+    bg_left.Close()
+
+    title = openshot.QtImageReader(dir_title+name_hash+".png")
+    title.Open()         # Open the reader
+    title_clip = video_photo_clip(vid=title, layer=4,location_x=-0.047, location_y=0.801,position=0,end=head_duration+main_timer)
+    t.AddClip(title_clip)
+
+    w = video_writer_init(tmp_video_dir+name_hash+"raw.mp4")
+    w.Open()
+    frames = int(t.info.fps)*int(main_timer)
+    for n in range(frames):
+        f=t.GetFrame(n)
+        w.WriteFrame(f)
+    t.Close()
+    w.Close()
+    print(name+"RAW DONE : www.choozmo.com:8168/"+video_sub_folder+name_hash+"raw.mp4")
+    #start adding sub
+    
+    #add sub
+    Ctr_Autosub.init()
+    Ctr_Autosub.generate_subtitles(tmp_video_dir+name_hash+"raw.mp4",'zh',listener_progress,output=tmp_video_dir+name_hash+"script.txt",concurrency=DEFAULT_CONCURRENCY,subtitle_file_format=DEFAULT_SUBTITLE_FORMAT)
+    
+    sub_dict = parse_script(tmp_video_dir+name_hash+"script.txt")
+    generate_subtitle_image_from_dict(sub_dict)
+    #sv_path = dir_subtitle + name_hash + '/' + str(script['index'])+'.png'
+
+    t = openshot.Timeline(1280, 720, openshot.Fraction(30000, 1000), 44100, 2, openshot.LAYOUT_STEREO)
+    t.Open()
+
+    raw = openshot.FFmpegReader(tmp_video_dir+name_hash+"raw.mp4")
+    raw.Open()
+    raw_clip = video_photo_clip(vid=raw,layer=2,position=0, end=raw.info.duration)
+    t.AddClip(raw_clip)
+    
+    sub_img_list[idx] = [None] * len(sub_dict)
+    sub_clip_list[idx] = [None] * len(sub_dict)
+    for sub_obj in sub_dict:
+        sub_img_list[sub_obj['index']] = openshot.QtImageReader(dir_subtitle + name_hash + '/' + str(sub_obj['index'])+'.png')
+        sub_img_list[sub_obj['index']].Open()
+        sub_clip_list[sub_obj['index']] = video_photo_clip(vid=sub_img_list[sub_obj['index']], layer=6,location_x=0.069, location_y=0.89,position=sub_obj['start'],end=math.ceil(sub_obj['duration']))
+        t.AddClip(sub_clip_list[idx][sub_idx])
+        sub_img_list[sub_obj['index']].Close()
+
+
+    os.remove(tmp_video_dir+name_hash+"raw.mp4")
+    os.remove(tmp_video_dir+name_hash+"script.txt")
+    anchor = openshot.FFmpegReader(dir_anchor+name_hash+"/0.mp4")
+    anchor.Open()
+    anchor_clip = video_photo_clip(vid=anchor,layer=4,scale_x=0.65,scale_y=0.65,
+            location_x=0.35,location_y=0.25,position=head_duration, end=anchor.info.duration,ck=ck_anchor,audio=False)
+    t.AddClip(anchor_clip)
+
+
+    w = video_writer_init(tmp_video_dir+name_hash+".mp4")
+    w.Open()
+    frames = int(t.info.fps)*int(main_timer)
+    for n in range(frames):
+        f=t.GetFrame(n)
+        w.WriteFrame(f)
+    t.Close()
+    w.Close()
+    print(name+"ALL DONE : www.choozmo.com:8168/"+video_sub_folder+name_hash+"raw.mp4")
+
+
 def anchor_video_v2(name_hash,name,text_content, image_urls,multiLang,avatar):
     print(os.getcwd())
     print('sub image made')
@@ -552,9 +730,9 @@ class video_service(rpyc.Service):
         anchor_video_v2(name_hash,name,text_content, image_urls,multiLang,avatar)
     def exposed_call_video_eng(self,name_hash,name,text_content, image_urls,sub_titles,avatar):
         anchor_video_eng(name_hash,name,text_content, image_urls,sub_titles,avatar)
-    def exposed_call_videoGen(self,name_hash,name,text_content, image_urls,multiLang,avatar):
-        print('ML:'+str(multiLang))
-        anchor_video_v2(name_hash,name,text_content, image_urls,multiLang,avatar)
+    def exposed_call_video_gen(self,name_hash,name,text_content, image_urls,multiLang,avatar):
+        print('ML:'+str(multiLang))#this is long video version,
+        video_gen(name_hash,name,text_content, image_urls,multiLang,avatar)
 
 
 

BIN
OpenshotService/pytranscriber/.DS_Store


+ 0 - 0
OpenshotService/pytranscriber/control/__init__.py


BIN
OpenshotService/pytranscriber/control/__pycache__/__init__.cpython-37.pyc


BIN
OpenshotService/pytranscriber/control/__pycache__/ctr_autosub.cpython-37.pyc


BIN
OpenshotService/pytranscriber/control/__pycache__/ctr_main.cpython-37.pyc


BIN
OpenshotService/pytranscriber/control/__pycache__/thread_cancel_autosub.cpython-37.pyc


BIN
OpenshotService/pytranscriber/control/__pycache__/thread_exec_autosub.cpython-37.pyc


+ 145 - 0
OpenshotService/pytranscriber/control/ctr_autosub.py

@@ -0,0 +1,145 @@
+from autosub import FLACConverter
+from autosub import SpeechRecognizer
+from autosub import extract_audio
+from autosub import find_speech_regions
+from autosub import DEFAULT_CONCURRENCY
+from autosub import DEFAULT_SUBTITLE_FORMAT
+from autosub import GOOGLE_SPEECH_API_KEY
+from autosub.formatters import FORMATTERS
+
+import multiprocessing
+import time
+import os
+
+from pytranscriber.util.util import MyUtil
+
+
+class Ctr_Autosub():
+
+    cancel = False
+
+    @staticmethod
+    def init():
+        Ctr_Autosub.cancel = False
+
+    @staticmethod
+    def is_operation_canceled():
+        return Ctr_Autosub.cancel
+
+
+    @staticmethod
+    def output_progress(listener_progress, str_task, progress_percent):
+        # only update progress if not requested to cancel
+        if not Ctr_Autosub.cancel:
+            listener_progress(str_task, progress_percent)
+
+    @staticmethod
+    def cancel_operation():
+        Ctr_Autosub.cancel = True
+
+        while Ctr_Autosub.step == 0:
+            time.sleep(0.1)
+
+        # the first step involves ffmpeg and cannot be stopped safely
+        if Ctr_Autosub.step == 1:
+            # close wait for threads to finish their work first
+            Ctr_Autosub.pool.close()
+            Ctr_Autosub.pool.join()
+
+        else:
+            # terminates the threads immediately
+            Ctr_Autosub.pool.terminate()
+            Ctr_Autosub.pool.join()
+
+    @staticmethod
+    def generate_subtitles(# pylint: disable=too-many-locals,too-many-arguments
+            source_path,
+            src_language,
+            listener_progress,
+            output=None,
+            concurrency=DEFAULT_CONCURRENCY,
+            subtitle_file_format=DEFAULT_SUBTITLE_FORMAT
+        ):
+
+        # windows not support forkserver... only spawn
+        if os.name != "nt" and "Darwin" in os.uname():
+            # necessary for running on MacOS
+            # method can be set only once, otherwise crash
+            #from python 3.8 above the default for macos is spawn and not fork
+            if 'spawn' != multiprocessing.get_start_method(allow_none=True):
+                multiprocessing.set_start_method('spawn')
+        Ctr_Autosub.cancel = False
+        Ctr_Autosub.step = 0
+        """
+        Given an input audio/video file, generate subtitles in the specified language and format.
+        """
+        audio_filename, audio_rate = extract_audio(source_path)
+
+        regions = find_speech_regions(audio_filename)
+
+        converter = FLACConverter(source_path=audio_filename)
+        recognizer = SpeechRecognizer(language=src_language, rate=audio_rate,
+                                      api_key=GOOGLE_SPEECH_API_KEY)
+        transcripts = []
+        if regions:
+            try:
+                if Ctr_Autosub.cancel:
+                    return -1
+
+                str_task_1 = "Step 1 of 2: Converting speech regions to FLAC files "
+                len_regions = len(regions)
+                extracted_regions = []
+                Ctr_Autosub.pool = multiprocessing.Pool(concurrency)
+                for i, extracted_region in enumerate(Ctr_Autosub.pool.imap(converter, regions)):
+                    Ctr_Autosub.step = 1
+                    extracted_regions.append(extracted_region)
+                    progress_percent = MyUtil.percentage(i, len_regions)
+                    Ctr_Autosub.output_progress(listener_progress, str_task_1, progress_percent)
+                if Ctr_Autosub.cancel:
+                    return -1
+                else:
+                    Ctr_Autosub.pool.close()
+                    Ctr_Autosub.pool.join()
+
+                str_task_2 = "Step 2 of 2: Performing speech recognition "
+                Ctr_Autosub.pool = multiprocessing.Pool(concurrency)
+                for i, transcript in enumerate(Ctr_Autosub.pool.imap(recognizer, extracted_regions)):
+                    Ctr_Autosub.step = 2
+                    transcripts.append(transcript)
+                    progress_percent = MyUtil.percentage(i, len_regions)
+                    Ctr_Autosub.output_progress(listener_progress, str_task_2, progress_percent)
+
+                if Ctr_Autosub.cancel:
+                    return -1
+                else:
+                    Ctr_Autosub.pool.close()
+                    Ctr_Autosub.pool.join()
+
+            except KeyboardInterrupt:
+                Ctr_Autosub.pbar.finish()
+                Ctr_Autosub.pool.terminate()
+                Ctr_Autosub.pool.join()
+                raise
+
+        timed_subtitles = [(r, t) for r, t in zip(regions, transcripts) if t]
+        formatter = FORMATTERS.get(subtitle_file_format)
+        formatted_subtitles = formatter(timed_subtitles)
+
+        dest = output
+
+        if not dest:
+            base = os.path.splitext(source_path)[0]
+            dest = "{base}.{format}".format(base=base, format=subtitle_file_format)
+
+        with open(dest, 'wb') as output_file:
+            output_file.write(formatted_subtitles.encode("utf-8"))
+
+        os.remove(audio_filename)
+
+        if Ctr_Autosub.cancel:
+            return -1
+        else:
+            Ctr_Autosub.pool.close()
+            Ctr_Autosub.pool.join()
+
+        return dest

+ 413 - 0
OpenshotService/pytranscriber/control/ctr_main.py

@@ -0,0 +1,413 @@
+'''
+   (C) 2019 Raryel C. Souza
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+'''
+
+from PyQt5 import QtCore, QtWidgets
+from PyQt5.QtWidgets import QFileDialog, QMessageBox
+from PyQt5.QtCore import Qt
+from pathlib import Path
+from pytranscriber.model.param_autosub import Param_Autosub
+from pytranscriber.util.util import MyUtil
+from pytranscriber.control.thread_exec_autosub import Thread_Exec_Autosub
+from pytranscriber.control.thread_cancel_autosub import Thread_Cancel_Autosub
+from pytranscriber.gui.gui import Ui_window
+import os
+
+
+class Ctr_Main():
+
+    def __init__(self):
+        import sys
+        app = QtWidgets.QApplication(sys.argv)
+        window = QtWidgets.QMainWindow()
+        self.objGUI = Ui_window()
+        self.objGUI.setupUi(window)
+        self.__initGUI()
+        window.setFixedSize(window.size())
+        window.show()
+        sys.exit(app.exec_())
+
+
+
+    def __initGUI(self):
+
+        #language selection list
+        list_languages =  [ "en-US - English (United States)",
+                            "en-AU - English (Australia)",
+                            "en-CA - English (Canada)",
+                            "en-GB - English (United Kingdom)",
+                            "en-HK - English (Hong Kong)",
+                            "en-IN - English (India)",
+                            "en-GB - English (Ireland)",
+                            "en-NZ - English (New Zealand)",
+                            "en-PH - English (Philippines)",
+                            "en-SG - English (Singapore)",
+                            "af - Afrikaans",
+                            "ar - Arabic",
+                            'ar-DZ - Arabic (Algeria)',
+                            'ar-EG - Arabic (Egypt)',
+                            'ar-IQ - Arabic (Iraq)',
+                            'ar-IS - Arabic (Israel)',
+                            'ar-JO - Arabic (Jordan)',
+                            'ar-KW - Arabic (Kuwait)',
+                            'ar-LB - Arabic (Lebanon)',
+                            'ar-MA - Arabic (Morocco)',
+                            'ar-OM - Arabic (Oman)',
+                            'ar-QA - Arabic (Qatar)',
+                            'ar-SA - Arabic (Saudi Arabia)',
+                            'ar-PS - Arabic (State of Palestine)',
+                            'ar-TN - Arabic (Tunisia)',
+                            'ar-AE - Arabic (United Arab Emirates)',
+                            'ar-YE - Arabic (Yemen)',
+                            "az - Azerbaijani",
+                            "be - Belarusian",
+                            "bg - Bulgarian",
+                            "bn - Bengali",
+                            "bs - Bosnian",
+                            "ca - Catalan",
+                            "ceb -Cebuano",
+                            "cs - Czech",
+                            "cy - Welsh",
+                            "da - Danish",
+                            "de - German",
+                            'de-AT - German (Austria)',
+                            'de-CH - German (Switzerland)',
+                            "el - Greek",
+                            "eo - Esperanto",
+                            'es-ES - Spanish (Spain)',
+                            'es-AR - Spanish (Argentina)',
+                            'es-BO - Spanish (Bolivia)',
+                            'es-CL - Spanish (Chile)',
+                            'es-CO - Spanish (Colombia)',
+                            'es-CR - Spanish (Costa Rica)',
+                            'es-DO - Spanish (Dominican Republic)',
+                            'es-EC - Spanish (Ecuador)',
+                            'es-GT - Spanish (Guatemala)',
+                            'es-HN - Spanish (Honduras)',
+                            'es-MX - Spanish (Mexico)',
+                            'es-NI - Spanish (Nicaragua)',
+                            'es-PA - Spanish (Panama)',
+                            'es-PE - Spanish (Peru)',
+                            'es-PR - Spanish (Puerto Rico)',
+                            'es-PY - Spanish (Paraguay)',
+                            'es-SV - Spanish (El Salvador)',
+                            'es-UY - Spanish (Uruguay)',
+                            'es-US - Spanish (United States)',
+                            'es-VE - Spanish (Venezuela)',
+                            "et - Estonian",
+                            "eu - Basque",
+                            "fa - Persian",
+                            'fil-PH - Filipino (Philippines)',
+                            "fi - Finnish",
+                            "fr - French",
+                            'fr-BE - French (Belgium)',
+                            'fr-CA - French (Canada)',
+                            'fr-CH - French (Switzerland)',
+                            "ga - Irish",
+                            "gl - Galician",
+                            "gu -Gujarati",
+                            "ha - Hausa",
+                            "hi - Hindi",
+                            "hmn - Hmong",
+                            "hr - Croatian",
+                            "ht - Haitian Creole",
+                            "hu - Hungarian",
+                            "hy - Armenian",
+                            "id - Indonesian",
+                            "ig - Igbo",
+                            "is - Icelandic",
+                            "it - Italian",
+                            'it-CH - Italian (Switzerland)',
+                            "iw - Hebrew",
+                            "ja - Japanese",
+                            "jw - Javanese",
+                            "ka - Georgian",
+                            "kk - Kazakh",
+                            "km - Khmer",
+                            "kn - Kannada",
+                            "ko - Korean",
+                            "la - Latin",
+                            "lo - Lao",
+                            "lt - Lithuanian",
+                            "lv - Latvian",
+                            "mg - Malagasy",
+                            "mi - Maori",
+                            "mk - Macedonian",
+                            "ml - Malayalam",
+                            "mn - Mongolian",
+                            "mr - Marathi",
+                            "ms - Malay",
+                            "mt - Maltese",
+                            "my - Myanmar (Burmese)",
+                            "ne - Nepali",
+                            "nl - Dutch",
+                            "no - Norwegian",
+                            "ny - Chichewa",
+                            "pa - Punjabi",
+                            "pl - Polish",
+                            "pt-BR - Portuguese (Brazil)",
+                            "pt-PT - Portuguese (Portugal)",
+                            "ro - Romanian",
+                            "ru - Russian",
+                            "si - Sinhala",
+                            "sk - Slovak",
+                            "sl - Slovenian",
+                            "so - Somali",
+                            "sq - Albanian",
+                            "sr - Serbian",
+                            "st - Sesotho",
+                            "su - Sudanese",
+                            "sv - Swedish",
+                            "sw - Swahili",
+                            "ta - Tamil",
+                            'ta-IN - Tamil (India)',
+                            'ta-MY - Tamil (Malaysia)',
+                            'ta-SG - Tamil (Singapore)',
+                            'ta-LK - Tamil (Sri Lanka)',
+                            "te - Telugu",
+                            "tg - Tajik",
+                            "th - Thai",
+                            "tl - Filipino",
+                            "tr - Turkish",
+                            "uk - Ukrainian",
+                            "ur - Urdu",
+                            "uz - Uzbek",
+                            "vi - Vietnamese",
+                            "yi - Yiddish",
+                            "yo - Yoruba",
+                            "yue-Hant-HK - Cantonese (Traditional, HK)",
+                            "zh - Chinese (Simplified, China)",
+                            "zh-HK - Chinese (Simplified, Hong Kong)",
+                            "zh-TW - Chinese (Traditional, Taiwan)",
+                            "zu - Zulu" ]
+
+        self.objGUI.cbSelectLang.addItems(list_languages)
+        self.__listenerProgress("", 0)
+
+        #default output folder at user desktop
+        userHome = Path.home()
+        pathOutputFolder = userHome / 'Desktop' / 'pyTranscriber'
+        self.objGUI.qleOutputFolder.setText(str(pathOutputFolder))
+
+        self.objGUI.bRemoveFile.setEnabled(False)
+
+        self.objGUI.bCancel.hide()
+
+        #button listeners
+        self.objGUI.bConvert.clicked.connect(self.__listenerBExec)
+        self.objGUI.bCancel.clicked.connect(self.__listenerBCancel)
+        self.objGUI.bRemoveFile.clicked.connect(self.__listenerBRemove)
+        self.objGUI.bSelectOutputFolder.clicked.connect(self.__listenerBSelectOuputFolder)
+        self.objGUI.bOpenOutputFolder.clicked.connect(self.__listenerBOpenOutputFolder)
+        self.objGUI.bSelectMedia.clicked.connect(self.__listenerBSelectMedia)
+
+        self.objGUI.actionLicense.triggered.connect(self.__listenerBLicense)
+        self.objGUI.actionDonation.triggered.connect(self.__listenerBDonation)
+        self.objGUI.actionAbout_pyTranscriber.triggered.connect(self.__listenerBAboutpyTranscriber)
+
+    def __resetGUIAfterSuccess(self):
+        self.__resetGUIAfterCancel()
+
+        self.objGUI.qlwListFilesSelected.clear()
+        self.objGUI.bConvert.setEnabled(False)
+        self.objGUI.bRemoveFile.setEnabled(False)
+
+    def __resetGUIAfterCancel(self):
+
+        self.__resetProgressBar()
+
+        self.objGUI.bSelectMedia.setEnabled(True)
+        self.objGUI.bSelectOutputFolder.setEnabled(True)
+        self.objGUI.cbSelectLang.setEnabled(True)
+        self.objGUI.chbxOpenOutputFilesAuto.setEnabled(True)
+
+        self.objGUI.bCancel.hide()
+        self.objGUI.bConvert.setEnabled(True)
+        self.objGUI.bRemoveFile.setEnabled(True)
+
+    def __lockButtonsDuringOperation(self):
+        self.objGUI.bConvert.setEnabled(False)
+        self.objGUI.bRemoveFile.setEnabled(False)
+        self.objGUI.bSelectMedia.setEnabled(False)
+        self.objGUI.bSelectOutputFolder.setEnabled(False)
+        self.objGUI.cbSelectLang.setEnabled(False)
+        self.objGUI.chbxOpenOutputFilesAuto.setEnabled(False)
+        QtCore.QCoreApplication.processEvents()
+
+    def __listenerProgress(self, str, percent):
+        self.objGUI.labelCurrentOperation.setText(str)
+        self.objGUI.progressBar.setProperty("value", percent)
+        QtCore.QCoreApplication.processEvents()
+
+    def __setProgressBarIndefinite(self):
+        self.objGUI.progressBar.setMinimum(0)
+        self.objGUI.progressBar.setMaximum(0)
+        self.objGUI.progressBar.setValue(0)
+
+    def __resetProgressBar(self):
+        self.objGUI.progressBar.setMinimum(0)
+        self.objGUI.progressBar.setMaximum(100)
+        self.objGUI.progressBar.setValue(0)
+        self.__listenerProgress("", 0)
+
+    def __updateProgressFileYofN(self, str):
+        self.objGUI.labelProgressFileIndex.setText(str)
+        QtCore.QCoreApplication.processEvents()
+
+    def __listenerBSelectOuputFolder(self):
+        fSelectDir = QFileDialog.getExistingDirectory(self.objGUI.centralwidget)
+        if fSelectDir:
+            self.objGUI.qleOutputFolder.setText(fSelectDir)
+
+    def __listenerBSelectMedia(self):
+        #options = QFileDialog.Options()
+        options = QFileDialog.DontUseNativeDialog
+        files, _ = QFileDialog.getOpenFileNames(self.objGUI.centralwidget, "Select media", "","All Media Files (*.mp3 *.mp4 *.wav *.m4a *.wma)")
+
+        if files:
+            self.objGUI.qlwListFilesSelected.addItems(files)
+
+            #enable the convert button only if list of files is not empty
+            self.objGUI.bConvert.setEnabled(True)
+            self.objGUI.bRemoveFile.setEnabled(True)
+
+
+    def __listenerBExec(self):
+        if not MyUtil.is_internet_connected():
+            self.__showErrorMessage("Error! Cannot reach Google Speech Servers. \n\n1) Please make sure you are connected to the internet. \n2) If you are in China or other place that blocks access to Google servers: please install and enable a desktop-wide VPN app like Windscribe before trying to use pyTranscriber!")
+        else:
+            #extracts the two letter lang_code from the string on language selection
+            selectedLanguage = self.objGUI.cbSelectLang.currentText()
+            indexSpace = selectedLanguage.index(" ")
+            langCode = selectedLanguage[:indexSpace]
+
+            listFiles = []
+            for i in range(self.objGUI.qlwListFilesSelected.count()):
+                listFiles.append(str(self.objGUI.qlwListFilesSelected.item(i).text()))
+
+            outputFolder = self.objGUI.qleOutputFolder.text()
+
+            if self.objGUI.chbxOpenOutputFilesAuto.checkState() == Qt.Checked:
+                boolOpenOutputFilesAuto = True
+            else:
+                boolOpenOutputFilesAuto = False
+
+            objParamAutosub = Param_Autosub(listFiles, outputFolder, langCode,
+                                            boolOpenOutputFilesAuto)
+
+            #execute the main process in separate thread to avoid gui lock
+            self.thread_exec = Thread_Exec_Autosub(objParamAutosub)
+
+            #connect signals from work thread to gui controls
+            self.thread_exec.signalLockGUI.connect(self.__lockButtonsDuringOperation)
+            self.thread_exec.signalResetGUIAfterSuccess.connect(self.__resetGUIAfterSuccess)
+            self.thread_exec.signalResetGUIAfterCancel.connect(self.__resetGUIAfterCancel)
+            self.thread_exec.signalProgress.connect(self.__listenerProgress)
+            self.thread_exec.signalProgressFileYofN.connect(self.__updateProgressFileYofN)
+            self.thread_exec.signalErrorMsg.connect(self.__showErrorMessage)
+            self.thread_exec.start()
+
+            #Show the cancel button
+            self.objGUI.bCancel.show()
+            self.objGUI.bCancel.setEnabled(True)
+
+    def __listenerBCancel(self):
+        self.objGUI.bCancel.setEnabled(False)
+        self.thread_cancel = Thread_Cancel_Autosub(self.thread_exec)
+
+        #Only if worker thread is running
+        if self.thread_exec and self.thread_exec.isRunning():
+            #reset progress indicator
+            self.__listenerProgress("Cancelling", 0)
+            self.__setProgressBarIndefinite()
+            self.__updateProgressFileYofN("")
+
+            #connect the terminate signal to resetGUI
+            self.thread_cancel.signalTerminated.connect(self.__resetGUIAfterCancel)
+            #run the cancel autosub operation in new thread
+            #to avoid progressbar freezing
+            self.thread_cancel.start()
+            self.thread_exec = None
+
+    def __listenerBRemove(self):
+        indexSelected = self.objGUI.qlwListFilesSelected.currentRow()
+        if indexSelected >= 0:
+            self.objGUI.qlwListFilesSelected.takeItem(indexSelected)
+
+        #if no items left disables the remove and convert button
+        if self.objGUI.qlwListFilesSelected.count() == 0:
+            self.objGUI.bRemoveFile.setEnabled(False)
+            self.objGUI.bConvert.setEnabled(False)
+
+    def __listenerBOpenOutputFolder(self):
+        pathOutputFolder = Path(self.objGUI.qleOutputFolder.text())
+
+        #if folder exists and is valid directory
+        if os.path.exists(pathOutputFolder) and os.path.isdir(pathOutputFolder):
+            MyUtil.open_file(pathOutputFolder)
+        else:
+            self.__showErrorMessage("Error! Invalid output folder.")
+
+    def __listenerBLicense(self):
+        self.__showInfoMessage("<html><body><a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GPL License</a><br><br>"
+                + "Copyright (C) 2019 Raryel C. Souza <raryel.costa at gmail.com><br>"
+                + "<br>This program is free software: you can redistribute it and/or modify<br>"
+                + "it under the terms of the GNU General Public License as published by<br>"
+                + "the Free Software Foundation, either version 3 of the License, or<br>"
+                + " any later version<br>"
+                + "<br>"
+                + "This program is distributed in the hope that it will be useful,<br>"
+                + "but WITHOUT ANY WARRANTY; without even the implied warranty of<br>"
+                + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the<br>"
+                + "GNU General Public License for more details.<br>"
+                + "<br>"
+                + "You should have received a copy of the GNU General Public License<br>"
+                + "along with this program.  If not, see <a href=\"https://www.gnu.org/licenses\">www.gnu.org/licenses</a>."
+                + "</body></html>", "License")
+
+    def __listenerBDonation(self):
+        self.__showInfoMessage("<html><body>"
+                + "pyTranscriber is developed as a hobby, so donations of any value are welcomed and essential for further improvements and fixes."
+                + "<br><br>If you feel that this software has been useful and would like to contribute for it to continue improve and have more features and fixes you can <a href=\"https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=YHB854YHPJCU8&item_name=Donation+pyTranscriber&currency_code=BRL\">DONATE VIA PAYPAL</a> or <a href=\"https://blockchain.com/btc/payment_request?address=153LcqV59paxEEJX7riLrEHQbE54vhcko9&amount=0.00026351&message=Donation to support pyTranscriber development\"> DONATE US$5 VIA BITCOIN</a>."
+                + "<br><br>Thanks in advance!"
+                + "</body></html>", "DONATIONS")
+
+    def __listenerBAboutpyTranscriber(self):
+        self.__showInfoMessage("<html><body>"
+                + "<a href=\"https://github.com/raryelcostasouza/pyTranscriber\">pyTranscriber</a> is an application that can be used "
+                + "to generate <b>automatic transcription / automatic subtitles </b>"
+                + "for audio/video files through a friendly graphical user interface. "
+                + "<br><br>"
+                + "The hard work of speech recognition is made by the <a href=\"https://cloud.google.com/speech/\">Google Speech Recognition API</a> "
+                + "using <a href=\"https://github.com/agermanidis/autosub\">Autosub</a>"
+                + "<br><br>pyTranscriber is developed as a hobby, so donations of any value are welcomed and essential for further improvements and fixes."
+                + "<br><br>If you feel that this software has been useful and would like to contribute for it to continue improve and have more features and fixes you can <a href=\"https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=YHB854YHPJCU8&item_name=Donation+pyTranscriber&currency_code=BRL\">DONATE VIA PAYPAL</a> or <a href=\"https://blockchain.com/btc/payment_request?address=153LcqV59paxEEJX7riLrEHQbE54vhcko9&amount=0.00026351&message=Donation to support pyTranscriber development\"> DONATE US$5 VIA BITCOIN</a>."
+                + "<br><br>Thanks in advance!"
+                + "</body></html>", "About pyTranscriber")
+
+
+    def __showInfoMessage(self, info_msg, title):
+        msg = QMessageBox()
+        msg.setIcon(QMessageBox.Information)
+
+        msg.setWindowTitle(title)
+        msg.setText(info_msg)
+        msg.exec()
+
+    def __showErrorMessage(self, errorMsg):
+        msg = QMessageBox()
+        msg.setIcon(QMessageBox.Critical)
+
+        msg.setWindowTitle("Error!")
+        msg.setText(errorMsg)
+        msg.exec()

+ 14 - 0
OpenshotService/pytranscriber/control/thread_cancel_autosub.py

@@ -0,0 +1,14 @@
+from PyQt5.QtCore import QThread
+from PyQt5.QtCore import pyqtSignal
+
+
+class Thread_Cancel_Autosub(QThread):
+    signalTerminated = pyqtSignal()
+
+    def __init__(self, pObjWT):
+        self.objWT = pObjWT
+        QThread.__init__(self)
+
+    def run(self):
+        self.objWT.cancel()
+        self.signalTerminated.emit()

+ 120 - 0
OpenshotService/pytranscriber/control/thread_exec_autosub.py

@@ -0,0 +1,120 @@
+'''
+   (C) 2019 Raryel C. Souza
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+'''
+
+from PyQt5.QtCore import QThread
+from PyQt5.QtCore import pyqtSignal
+from pathlib import Path
+from pytranscriber.util.srtparser import SRTParser
+from pytranscriber.util.util import MyUtil
+from pytranscriber.control.ctr_autosub import Ctr_Autosub
+import os
+
+
+class Thread_Exec_Autosub(QThread):
+    signalLockGUI = pyqtSignal()
+    signalResetGUIAfterCancel = pyqtSignal()
+    signalResetGUIAfterSuccess = pyqtSignal()
+    signalProgress = pyqtSignal(str, int)
+    signalProgressFileYofN = pyqtSignal(str)
+    signalErrorMsg = pyqtSignal(str)
+
+    def __init__(self, objParamAutosub):
+        self.objParamAutosub = objParamAutosub
+        self.running = True
+        QThread.__init__(self)
+
+    def __updateProgressFileYofN(self, currentIndex, countFiles ):
+        self.signalProgressFileYofN.emit("File " + str(currentIndex+1) + " of " +str(countFiles))
+
+    def listenerProgress(self, string, percent):
+        self.signalProgress.emit(string, percent)
+
+    def __generatePathOutputFile(self, sourceFile):
+        #extract the filename without extension from the path
+        base = os.path.basename(sourceFile)
+        #[0] is filename, [1] is file extension
+        fileName = os.path.splitext(base)[0]
+
+        #the output file has same name as input file, located on output Folder
+        #with extension .srt
+        pathOutputFolder = Path(self.objParamAutosub.outputFolder)
+        outputFileSRT = pathOutputFolder / (fileName + ".srt")
+        outputFileTXT = pathOutputFolder / (fileName + ".txt")
+        return [outputFileSRT, outputFileTXT]
+
+    def __runAutosubForMedia(self, index, langCode):
+        sourceFile = self.objParamAutosub.listFiles[index]
+        outputFiles = self.__generatePathOutputFile(sourceFile)
+        outputFileSRT = outputFiles[0]
+        outputFileTXT = outputFiles[1]
+
+        #run autosub
+        fOutput = Ctr_Autosub.generate_subtitles(source_path = sourceFile,
+                                    output = outputFileSRT,
+                                    src_language = langCode,
+                                    listener_progress = self.listenerProgress)
+        #if nothing was returned
+        if not fOutput:
+            self.signalErrorMsg.emit("Error! Unable to generate subtitles for file " + sourceFile + ".")
+        elif fOutput != -1:
+            #if the operation was not canceled
+
+            #updated the progress message
+            self.listenerProgress("Finished", 100)
+
+            #parses the .srt subtitle file and export text to .txt file
+            SRTParser.extractTextFromSRT(str(outputFileSRT))
+
+            if self.objParamAutosub.boolOpenOutputFilesAuto:
+                #open both SRT and TXT output files
+                MyUtil.open_file(outputFileTXT)
+                MyUtil.open_file(outputFileSRT)
+
+    def __loopSelectedFiles(self):
+        self.signalLockGUI.emit()
+
+        langCode = self.objParamAutosub.langCode
+
+        #if output directory does not exist, creates it
+        pathOutputFolder = Path(self.objParamAutosub.outputFolder)
+
+        if not os.path.exists(pathOutputFolder):
+            os.mkdir(pathOutputFolder)
+        #if there the output file is not a directory
+        if not os.path.isdir(pathOutputFolder):
+            #force the user to select a different output directory
+            self.signalErrorMsg.emit("Error! Invalid output folder. Please choose another one.")
+        else:
+            #go ahead with autosub process
+            nFiles = len(self.objParamAutosub.listFiles)
+            for i in range(nFiles):
+                #does not continue the loop if user clicked cancel button
+                if not Ctr_Autosub.is_operation_canceled():
+                    self.__updateProgressFileYofN(i, nFiles)
+                    self.__runAutosubForMedia(i, langCode)
+
+            #if operation is canceled does not clear the file list
+            if Ctr_Autosub.is_operation_canceled():
+                self.signalResetGUIAfterCancel.emit()
+            else:
+                self.signalResetGUIAfterSuccess.emit()
+
+
+    def run(self):
+        Ctr_Autosub.init()
+        self.__loopSelectedFiles()
+        self.running = False
+
+    def cancel(self):
+       Ctr_Autosub.cancel_operation()

+ 0 - 0
OpenshotService/pytranscriber/gui/__init__.py


BIN
OpenshotService/pytranscriber/gui/__pycache__/__init__.cpython-37.pyc


BIN
OpenshotService/pytranscriber/gui/__pycache__/gui.cpython-37.pyc


+ 120 - 0
OpenshotService/pytranscriber/gui/gui.py

@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'gui.ui'
+#
+# Created by: PyQt5 UI code generator 5.13.1
+#
+# WARNING! All changes made in this file will be lost!
+
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_window(object):
+    def setupUi(self, window):
+        window.setObjectName("window")
+        window.resize(1045, 487)
+        self.centralwidget = QtWidgets.QWidget(window)
+        self.centralwidget.setObjectName("centralwidget")
+        self.bSelectMedia = QtWidgets.QPushButton(self.centralwidget)
+        self.bSelectMedia.setGeometry(QtCore.QRect(10, 10, 141, 34))
+        self.bSelectMedia.setObjectName("bSelectMedia")
+        self.bConvert = QtWidgets.QPushButton(self.centralwidget)
+        self.bConvert.setEnabled(False)
+        self.bConvert.setGeometry(QtCore.QRect(200, 290, 341, 34))
+        self.bConvert.setObjectName("bConvert")
+        self.progressBar = QtWidgets.QProgressBar(self.centralwidget)
+        self.progressBar.setGeometry(QtCore.QRect(20, 340, 1021, 23))
+        self.progressBar.setProperty("value", 0)
+        self.progressBar.setObjectName("progressBar")
+        self.labelCurrentOperation = QtWidgets.QLabel(self.centralwidget)
+        self.labelCurrentOperation.setGeometry(QtCore.QRect(170, 350, 871, 41))
+        self.labelCurrentOperation.setText("")
+        self.labelCurrentOperation.setObjectName("labelCurrentOperation")
+        self.bOpenOutputFolder = QtWidgets.QPushButton(self.centralwidget)
+        self.bOpenOutputFolder.setGeometry(QtCore.QRect(550, 290, 241, 34))
+        self.bOpenOutputFolder.setObjectName("bOpenOutputFolder")
+        self.bSelectOutputFolder = QtWidgets.QPushButton(self.centralwidget)
+        self.bSelectOutputFolder.setGeometry(QtCore.QRect(10, 180, 141, 34))
+        self.bSelectOutputFolder.setObjectName("bSelectOutputFolder")
+        self.qleOutputFolder = QtWidgets.QLineEdit(self.centralwidget)
+        self.qleOutputFolder.setGeometry(QtCore.QRect(160, 180, 861, 32))
+        self.qleOutputFolder.setText("")
+        self.qleOutputFolder.setReadOnly(True)
+        self.qleOutputFolder.setObjectName("qleOutputFolder")
+        self.groupBox = QtWidgets.QGroupBox(self.centralwidget)
+        self.groupBox.setGeometry(QtCore.QRect(160, 10, 871, 161))
+        self.groupBox.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
+        self.groupBox.setFlat(False)
+        self.groupBox.setCheckable(False)
+        self.groupBox.setObjectName("groupBox")
+        self.qlwListFilesSelected = QtWidgets.QListWidget(self.groupBox)
+        self.qlwListFilesSelected.setGeometry(QtCore.QRect(10, 30, 851, 121))
+        self.qlwListFilesSelected.setObjectName("qlwListFilesSelected")
+        self.bRemoveFile = QtWidgets.QPushButton(self.centralwidget)
+        self.bRemoveFile.setGeometry(QtCore.QRect(10, 50, 141, 34))
+        self.bRemoveFile.setObjectName("bRemoveFile")
+        self.labelProgressFileIndex = QtWidgets.QLabel(self.centralwidget)
+        self.labelProgressFileIndex.setGeometry(QtCore.QRect(30, 350, 131, 41))
+        self.labelProgressFileIndex.setText("")
+        self.labelProgressFileIndex.setObjectName("labelProgressFileIndex")
+        self.bCancel = QtWidgets.QPushButton(self.centralwidget)
+        self.bCancel.setGeometry(QtCore.QRect(470, 390, 108, 36))
+        self.bCancel.setObjectName("bCancel")
+        self.chbxOpenOutputFilesAuto = QtWidgets.QCheckBox(self.centralwidget)
+        self.chbxOpenOutputFilesAuto.setGeometry(QtCore.QRect(10, 220, 291, 32))
+        self.chbxOpenOutputFilesAuto.setChecked(True)
+        self.chbxOpenOutputFilesAuto.setObjectName("chbxOpenOutputFilesAuto")
+        self.horizontalLayoutWidget = QtWidgets.QWidget(self.centralwidget)
+        self.horizontalLayoutWidget.setGeometry(QtCore.QRect(200, 250, 591, 34))
+        self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
+        self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
+        self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0)
+        self.horizontalLayout_5.setObjectName("horizontalLayout_5")
+        self.labelSelectLang = QtWidgets.QLabel(self.horizontalLayoutWidget)
+        self.labelSelectLang.setObjectName("labelSelectLang")
+        self.horizontalLayout_5.addWidget(self.labelSelectLang)
+        self.cbSelectLang = QtWidgets.QComboBox(self.horizontalLayoutWidget)
+        self.cbSelectLang.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
+        self.cbSelectLang.setObjectName("cbSelectLang")
+        self.horizontalLayout_5.addWidget(self.cbSelectLang)
+        window.setCentralWidget(self.centralwidget)
+        self.menubar = QtWidgets.QMenuBar(window)
+        self.menubar.setGeometry(QtCore.QRect(0, 0, 1045, 34))
+        self.menubar.setObjectName("menubar")
+        self.menuAbout = QtWidgets.QMenu(self.menubar)
+        self.menuAbout.setObjectName("menuAbout")
+        window.setMenuBar(self.menubar)
+        self.statusbar = QtWidgets.QStatusBar(window)
+        self.statusbar.setObjectName("statusbar")
+        window.setStatusBar(self.statusbar)
+        self.actionLicense = QtWidgets.QAction(window)
+        self.actionLicense.setObjectName("actionLicense")
+        self.actionDonation = QtWidgets.QAction(window)
+        self.actionDonation.setObjectName("actionDonation")
+        self.actionAbout_pyTranscriber = QtWidgets.QAction(window)
+        self.actionAbout_pyTranscriber.setObjectName("actionAbout_pyTranscriber")
+        self.menuAbout.addAction(self.actionLicense)
+        self.menuAbout.addAction(self.actionDonation)
+        self.menuAbout.addAction(self.actionAbout_pyTranscriber)
+        self.menubar.addAction(self.menuAbout.menuAction())
+
+        self.retranslateUi(window)
+        QtCore.QMetaObject.connectSlotsByName(window)
+
+    def retranslateUi(self, window):
+        _translate = QtCore.QCoreApplication.translate
+        window.setWindowTitle(_translate("window", "pyTranscriber - v1.6 - 21/01/2020"))
+        self.bSelectMedia.setText(_translate("window", "Select file(s)"))
+        self.bConvert.setText(_translate("window", "Transcribe Audio / Generate Subtitles"))
+        self.bOpenOutputFolder.setText(_translate("window", "Open Output Folder"))
+        self.bSelectOutputFolder.setText(_translate("window", "Output Location"))
+        self.groupBox.setTitle(_translate("window", "&List of files to generate transcribe audio / generate subtitles"))
+        self.bRemoveFile.setText(_translate("window", "Remove file(s)"))
+        self.bCancel.setText(_translate("window", "Cancel"))
+        self.chbxOpenOutputFilesAuto.setText(_translate("window", "Open output files automatically"))
+        self.labelSelectLang.setText(_translate("window", "Audio Language:"))
+        self.menuAbout.setTitle(_translate("window", "Abo&ut"))
+        self.actionLicense.setText(_translate("window", "&License"))
+        self.actionDonation.setText(_translate("window", "&DONATIONS"))
+        self.actionAbout_pyTranscriber.setText(_translate("window", "&About pyTranscriber"))

+ 266 - 0
OpenshotService/pytranscriber/gui/gui.ui

@@ -0,0 +1,266 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>window</class>
+ <widget class="QMainWindow" name="window">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>1045</width>
+    <height>487</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>pyTranscriber - v1.5 - 07/12/2020</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <widget class="QPushButton" name="bSelectMedia">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>10</y>
+      <width>141</width>
+      <height>34</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Select file(s)</string>
+    </property>
+   </widget>
+   <widget class="QPushButton" name="bConvert">
+    <property name="enabled">
+     <bool>false</bool>
+    </property>
+    <property name="geometry">
+     <rect>
+      <x>200</x>
+      <y>290</y>
+      <width>341</width>
+      <height>34</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Transcribe Audio / Generate Subtitles</string>
+    </property>
+   </widget>
+   <widget class="QProgressBar" name="progressBar">
+    <property name="geometry">
+     <rect>
+      <x>20</x>
+      <y>340</y>
+      <width>1021</width>
+      <height>23</height>
+     </rect>
+    </property>
+    <property name="value">
+     <number>0</number>
+    </property>
+   </widget>
+   <widget class="QLabel" name="labelCurrentOperation">
+    <property name="geometry">
+     <rect>
+      <x>170</x>
+      <y>350</y>
+      <width>871</width>
+      <height>41</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string/>
+    </property>
+   </widget>
+   <widget class="QPushButton" name="bOpenOutputFolder">
+    <property name="geometry">
+     <rect>
+      <x>550</x>
+      <y>290</y>
+      <width>241</width>
+      <height>34</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Open Output Folder</string>
+    </property>
+   </widget>
+   <widget class="QPushButton" name="bSelectOutputFolder">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>180</y>
+      <width>141</width>
+      <height>34</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Output Location</string>
+    </property>
+   </widget>
+   <widget class="QLineEdit" name="qleOutputFolder">
+    <property name="geometry">
+     <rect>
+      <x>160</x>
+      <y>180</y>
+      <width>861</width>
+      <height>32</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string/>
+    </property>
+    <property name="readOnly">
+     <bool>true</bool>
+    </property>
+   </widget>
+   <widget class="QGroupBox" name="groupBox">
+    <property name="geometry">
+     <rect>
+      <x>160</x>
+      <y>10</y>
+      <width>871</width>
+      <height>161</height>
+     </rect>
+    </property>
+    <property name="title">
+     <string>&amp;List of files to generate transcribe audio / generate subtitles</string>
+    </property>
+    <property name="alignment">
+     <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+    </property>
+    <property name="flat">
+     <bool>false</bool>
+    </property>
+    <property name="checkable">
+     <bool>false</bool>
+    </property>
+    <widget class="QListWidget" name="qlwListFilesSelected">
+     <property name="geometry">
+      <rect>
+       <x>10</x>
+       <y>30</y>
+       <width>851</width>
+       <height>121</height>
+      </rect>
+     </property>
+    </widget>
+   </widget>
+   <widget class="QPushButton" name="bRemoveFile">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>50</y>
+      <width>141</width>
+      <height>34</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Remove file(s)</string>
+    </property>
+   </widget>
+   <widget class="QLabel" name="labelProgressFileIndex">
+    <property name="geometry">
+     <rect>
+      <x>30</x>
+      <y>350</y>
+      <width>131</width>
+      <height>41</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string/>
+    </property>
+   </widget>
+   <widget class="QPushButton" name="bCancel">
+    <property name="geometry">
+     <rect>
+      <x>470</x>
+      <y>390</y>
+      <width>108</width>
+      <height>36</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Cancel</string>
+    </property>
+   </widget>
+   <widget class="QCheckBox" name="chbxOpenOutputFilesAuto">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>220</y>
+      <width>291</width>
+      <height>32</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Open output files automatically</string>
+    </property>
+    <property name="checked">
+     <bool>true</bool>
+    </property>
+   </widget>
+   <widget class="QWidget" name="horizontalLayoutWidget">
+    <property name="geometry">
+     <rect>
+      <x>200</x>
+      <y>250</y>
+      <width>591</width>
+      <height>34</height>
+     </rect>
+    </property>
+    <layout class="QHBoxLayout" name="horizontalLayout_5">
+     <item>
+      <widget class="QLabel" name="labelSelectLang">
+       <property name="text">
+        <string>Audio Language:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="cbSelectLang">
+       <property name="sizeAdjustPolicy">
+        <enum>QComboBox::AdjustToContents</enum>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </widget>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>1045</width>
+     <height>34</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuAbout">
+    <property name="title">
+     <string>Abo&amp;ut</string>
+    </property>
+    <addaction name="actionLicense"/>
+    <addaction name="actionDonate"/>
+    <addaction name="actionAbout_pyTranscriber"/>
+   </widget>
+   <addaction name="menuAbout"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+  <action name="actionLicense">
+   <property name="text">
+    <string>&amp;License</string>
+   </property>
+  </action>
+  <action name="actionDonate">
+   <property name="text">
+    <string>&amp;DONATIONS</string>
+   </property>
+  </action>
+  <action name="actionAbout_pyTranscriber">
+   <property name="text">
+    <string>&amp;About pyTranscriber</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 0 - 0
OpenshotService/pytranscriber/model/__init__.py


BIN
OpenshotService/pytranscriber/model/__pycache__/__init__.cpython-37.pyc


BIN
OpenshotService/pytranscriber/model/__pycache__/param_autosub.cpython-37.pyc


+ 22 - 0
OpenshotService/pytranscriber/model/param_autosub.py

@@ -0,0 +1,22 @@
+'''
+   (C) 2019 Raryel C. Souza
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+'''
+
+class Param_Autosub():
+
+    def __init__(self, listFiles, outputFolder, langCode,
+                boolOpenOutputFilesAuto):
+        self.listFiles = listFiles
+        self.outputFolder = outputFolder
+        self.langCode = langCode
+        self.boolOpenOutputFilesAuto = boolOpenOutputFilesAuto

+ 0 - 0
OpenshotService/pytranscriber/util/__init__.py


BIN
OpenshotService/pytranscriber/util/__pycache__/__init__.cpython-37.pyc


BIN
OpenshotService/pytranscriber/util/__pycache__/srtparser.cpython-37.pyc


BIN
OpenshotService/pytranscriber/util/__pycache__/util.cpython-37.pyc


+ 49 - 0
OpenshotService/pytranscriber/util/srtparser.py

@@ -0,0 +1,49 @@
+'''
+   (C) 2019 Raryel C. Souza
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+'''
+
+import re, sys
+
+class SRTParser(object):
+    @staticmethod
+    def extractTextFromSRT(fileSRT):
+        file_name = fileSRT
+        file_encoding = 'utf-8'
+
+        #loop through the lines for parsing
+        with open(file_name, encoding=file_encoding, errors='replace') as f:
+            lines = f.readlines()
+            new_lines = SRTParser.clean_up(lines)
+            new_file_name = file_name[:-4] + '.txt'
+
+        #write parsed txt file
+        with open(new_file_name, 'w', encoding=file_encoding) as f:
+            for line in new_lines:
+                f.write(line)
+
+    @staticmethod
+    def clean_up(lines):
+        regexSubtitleIndexNumber = re.compile("[0-9]+")
+
+        new_lines = []
+        for line in lines[1:]:
+            #if line empty or
+            #if line contains --> or
+            #if line matches the subtitle index regex
+            #then skip line
+            if (not line or not line.strip()) or ("-->" in line) or regexSubtitleIndexNumber.match(line):
+                continue
+            else:
+                #append line
+                new_lines.append(line)
+        return new_lines

+ 44 - 0
OpenshotService/pytranscriber/util/util.py

@@ -0,0 +1,44 @@
+'''
+   (C) 2019 Raryel C. Souza
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+'''
+
+import platform
+import os
+import subprocess
+import socket
+
+class MyUtil(object):
+    @staticmethod
+    def open_file(path):
+        if platform.system() == "Windows":
+            os.startfile(path)
+        elif platform.system() == "Darwin":
+            subprocess.Popen(["open", path])
+        else:
+            subprocess.Popen(["xdg-open", path])
+
+    @staticmethod
+    def is_internet_connected():
+        try:
+            # connect to the host -- tells us if the host is actually
+            # reachable
+            s = socket.create_connection(("www.google.com", 80), 2)
+            s.close()
+            return True
+        except OSError:
+            pass
+        return False
+
+    @staticmethod
+    def percentage(currentval, maxval):
+        return 100 * currentval / float(maxval)

BIN
api/.DS_Store


+ 85 - 0
api/main.py

@@ -254,6 +254,34 @@ async def make_anchor_video_gSlide(req:models.gSlide_req,token: str = Depends(oa
     x.start()
     return {"msg":"製作影片需要時間,請您耐心等候,成果會傳送至LINE群組中"} 
 
+@app.post("/make_anchor_video_long")
+async def make_anchor_video_long(req:models.request,token: str = Depends(oauth2_scheme)):
+    if len(req.image_urls) != len(req.text_content):
+        return {'msg':'副標題數量、圖片(影片)數量以及台詞數量必須一致'}
+    for idx in range(len(req.image_urls)):
+        if 'http' not in req.image_urls[idx]:
+            req.image_urls[idx] = 'http://'+req.image_urls[idx]
+    if req.multiLang==0:
+        for txt in req.text_content:
+            if re.search('[a-zA-Z]', txt) !=None:
+                print('語言錯誤')
+                return {'msg':'輸入字串不能包含英文字!'}
+    name_hash = str(time.time()).replace('.','')
+    for imgu in req.image_urls:
+        try:
+            if get_url_type(imgu) =='video/mp4':
+                r=requests.get(imgu)
+            else:
+                im = Image.open(requests.get(imgu, stream=True).raw)
+                im= im.convert("RGB")
+        except:
+            return {'msg':"無法辨別圖片網址"+imgu}
+    user_id = get_user_id(token)
+    save_history(req,name_hash,user_id)
+    x = threading.Thread(target=gen_video_long_queue, args=(name_hash,req.name, req.text_content, req.image_urls,int(req.avatar),req.multiLang,user_id))
+    x.start()
+    return {"msg":"製作影片需要時間,請您耐心等候,成果會傳送至LINE群組中"} 
+
 @app.post("/make_anchor_video")
 async def make_anchor_video(req:models.request,token: str = Depends(oauth2_scheme)):
     if len(req.image_urls) != len(req.text_content):
@@ -454,6 +482,63 @@ def gen_video_eng(name_hash,name,text_content, image_urls,sub_titles,avatar):
     shutil.copy(tmp_video_dir+name_hash+'.mp4',video_dest+name_hash+'.mp4')
     os.remove(tmp_video_dir+name_hash+'.mp4')
 
+def gen_video_long_queue(name_hash,name,text_content, image_urls,avatar,multiLang,user_id):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    time_stamp = datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d %H:%M:%S")
+    txt_content_seperate_by_dot = ''
+    for txt in text_content:
+        txt_content_seperate_by_dot += txt+","
+    txt_content_seperate_by_dot = txt_content_seperate_by_dot[:-1]
+    img_urls_seperate_by_dot = ''
+    for iurl in image_urls:
+        img_urls_seperate_by_dot += iurl+","
+    img_urls_seperate_by_dot = img_urls_seperate_by_dot[:-1]
+    
+    db['video_queue'].insert({'name_hash':name_hash,'name':name,'text_content':txt_content_seperate_by_dot,'image_urls':img_urls_seperate_by_dot,'multiLang':multiLang,'avatar':avatar,'timestamp':time_stamp})
+    while True:
+        
+        if first(db.query('SELECT * FROM video_queue_status'))['status'] == 1:#only one row in this table, which is the id 1 one
+            print('another process running, leave loop')#1 means already running
+            break
+        if first(db.query('SELECT COUNT(1) FROM video_queue'))['COUNT(1)'] == 0:
+            print('all finish, leave loop')
+            break
+        top1 = first(db.query('SELECT * FROM video_queue'))
+        try:
+        # if True:
+            db.query('UPDATE video_queue_status SET status = 1;')
+            c = rpyc.connect("localhost", 8858)
+            c._config['sync_request_timeout'] = None
+            remote_svc = c.root
+            my_answer = remote_svc.call_video_gen(top1['name_hash'],top1['name'],top1['text_content'].split(','), top1['image_urls'].split(','),top1['multiLang'],top1['avatar']) # method call
+            shutil.copy(tmp_video_dir+top1['name_hash']+'.mp4',video_dest+top1['name_hash']+'.mp4')
+            os.remove(tmp_video_dir+top1['name_hash']+'.mp4')
+            vid_duration = VideoFileClip(video_dest+top1['name_hash']+'.mp4').duration
+            user_obj = first(db.query('SELECT * FROM users where id ="'+str(user_id)+'"'))
+            line_token = user_obj['line_token']         # aa
+            left_time = user_obj['left_time']
+            email = user_obj['email']
+            print('left_time is '+str(left_time))
+            if left_time is None:
+                left_time = 5*60
+            if left_time < vid_duration:
+                msg = '您本月額度剩下'+str(left_time)+'秒,此部影片有'+str(vid_duration)+'秒, 若要繼續產生影片請至 192.168.1.106:8887/confirm_add_value?name_hash='+name_hash+' 加值'
+                print(msg)
+                msg =msg.encode(encoding='utf-8')
+                mailer.send(msg, email)
+                notify_line_user(msg, line_token)
+            else:
+                left_time = left_time - vid_duration
+                db.query('UPDATE users SET left_time ='+str(left_time)+' WHERE id='+str(user_id)+';')
+                #notify_group(name+"的影片已經產生完成囉! www.choozmo.com:8168/"+video_sub_folder+name_hash+".mp4")
+                notify_line_user(name+"的影片已經產生完成囉! www.choozmo.com:8168/"+video_sub_folder+name_hash+".mp4", line_token)
+        except Exception as e:
+            logging.error(traceback.format_exc())
+            print('video generation error')
+            notify_group('影片錯誤')
+        db['video_queue'].delete(id=top1['id'])
+        db.query('UPDATE video_queue_status SET status = 0')
+
 def gen_video_queue(name_hash,name,text_content, image_urls,avatar,multiLang,user_id):
     db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
     time_stamp = datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d %H:%M:%S")

+ 48 - 0
api/static/script_util.js

@@ -126,6 +126,54 @@ $(".next").click(function () {
   result = xhr.send(objstr);
 });
 
+$("#sendBTN").click(function () {
+  button.setAttribute('disabled', '');
+  setTimeout(function () {
+    button.removeAttribute('disabled')
+  }, 4000);
+  avatar = $('.avatar').val();
+  name_title = $('.title_new').val();
+  txtARR = [];
+  imgARR = [];
+  var step;
+  let contentIdx = document.querySelectorAll(".txtsrc").length;
+  for (let i = 1; i < (contentIdx + 1); i++) {
+    if ($(`.txtsrc${i}`).val() != "") {
+      txtARR.push($(`.txtsrc${i}`).val())
+    }
+  }
+  let imgIdx = document.querySelectorAll(".imgsrc").length;
+  for (let i = 1; i < (imgIdx + 1); i++) {
+    if ($(`.imgsrc${i}`).val() != "") {
+      imgARR.push($(`.imgsrc${i}`).val())
+    }
+  }
+  multiLang = 0
+  if ($('#multiLang').prop("checked")) {multiLang = 1;}
+  dataOBJ = { "name": name_title, "text_content": txtARR, "image_urls": imgARR, "avatar": avatar,"multiLang":multiLang, "client_id": client_id }
+  objstr = JSON.stringify(dataOBJ);
+  console.log(dataOBJ)
+  jwt_token =  get_jwt_token()
+  var xhr = new XMLHttpRequest();
+  xhr.open("POST", "/make_anchor_video_long");
+  xhr.setRequestHeader("accept", "application/json");
+  xhr.setRequestHeader("Authorization","Bearer "+jwt_token)
+  xhr.setRequestHeader("Content-Type", "application/json");
+  xhr.onreadystatechange = function () {
+    if (xhr.readyState === 4) {
+      Swal.fire({
+        title: "資料已送出",
+        icon: 'success',
+        text: '資料已傳送,請耐心等候',
+        confirmButtonColor: '#3085d6',
+      });
+    }
+  };
+  var data = renderXHR_data(dataOBJ)
+  console.log(data)
+  result = xhr.send(objstr);
+});
+
 const slide_button = document.querySelector('#send_slide');
 $("#send_slide").click(function () {
   slide_button.setAttribute('disabled', '');

+ 170 - 0
api/templates/make_video_long.html

@@ -0,0 +1,170 @@
+
+ {% extends "index.html" %}
+{% block title %}Login{% endblock %}
+{% block head %}
+{{ super() }}
+{% endblock %}
+{% block content %}
+
+
+    <!-- content -->
+
+    <div class="container-fluid">
+      <div id="mySidenav" class="sidenav">
+        <!-- <a href="javascript:void(0)" class="closebtn" onclick="closeNav()">&times;</a> -->
+        <h2 class="go_title" href="/index"><a class="nav-link active" aria-current="page" href="/index">AI Spokesgirl</a></h2>
+        <ul class="nav-list">
+          <li class="nav-list-item pb-1 mb-3" data-bs-toggle="modal" data-bs-target="#howto"><i class="fas fa-book-open me-2"></i><lan set-lan="html:usage_intro">使用說明</lan></li>
+          <li class="nav-list-item pb-1" data-bs-toggle="modal" data-bs-target="#history" onclick="openNav()"><i class="fas fa-history me-2"></i><lan set-lan="html:history">歷史紀錄</lan></li>
+        </ul>
+        <p class="right-text text-white d-inline-block">Choozmo All Rights Reserved</p>
+      </div>
+      <!-- <span style="font-size:30px;cursor:pointer" onclick="openNav()">&#9776; 過去紀錄</span> -->
+      <div class="content ms-auto">
+        <form id="msform">
+          <div class="linker__box">
+            <p set-lan="html:preview_videos">預覽影片</p>
+            <i class="fas fa-link"></i>
+            <a id='linker' style="display: none;" class="ms-2" set-lan="html:video_link">影片連結</a>
+          </div>
+          <!-- fieldsets -->
+          <fieldset>
+            <h3 class="fs-subtitle"><lan set-lan="html:video_title">標題</lan><img class="ms-1" src="static/img/question.png" alt="" data-bs-toggle="tooltip" data-bs-placement="right" title="將作為影片的內嵌標題"></h3>            
+            <input id=title type="text" name='t1' class='title_new' value="" placeholder="標題" /> <br/>
+          </fieldset>
+          <fieldset>
+            <h3  class="fs-subtitle"><lan set-lan="html:choose_character">選擇人物</lan><img class="ms-1" src="static/img/question.png" alt="" data-bs-toggle="tooltip" data-bs-placement="right" title="將作為影片的講者"></h3>            
+            <select id="avatar" class='avatar'>
+              <option set-lan="html:p_choose_character" value="請選擇人物" selected="selected" disabled>>請選擇人物</option>              
+              <option value="7">Peggy</option>
+              <option value="8">Stacy</option>
+              <option value="10">Nina黑</option>
+              <option value="9">Nina灰</option>
+              <option value="11">Summer韓小夏</option>
+              <option value="12">Jocelyn</option>
+              <option value="12">Angela</option>
+            </select>
+            <div class="owl-carousel owl-theme">
+              <div class="card item" data-avatar="Peggy" data-img="peggy">
+                <div class="imgfr"><img src="static/img/peggy.webp" class="card-img-top" alt="..."></div>
+                <div class="card-body">
+                  <h5 class="card-title">Peggy</h5>
+                </div>
+              </div>
+              <div class="card item" data-avatar="Stacy" data-img="stacy">
+                <div class="imgfr"><img src="static/img/stacy.webp" class="card-img-top" alt="..."></div>
+                <div class="card-body">
+                  <h5 class="card-title">Stacy</h5>
+                </div>
+              </div>
+              <div class="card item" data-avatar="Nina黑" data-img="ninablack">
+                <div class="imgfr"><img src="static/img/ninablack.webp" class="card-img-top" alt="..."></div>
+                <div class="card-body">
+                  <h5 class="card-title">Nina黑</h5>
+                </div>
+              </div>
+              <div class="card item" data-avatar="Nina灰" data-img="ninawhite">
+                <div class="imgfr"><img src="static/img/ninawhite.webp" class="card-img-top" alt="..."></div>
+                <div class="card-body">
+                  <h5 class="card-title">Nina灰</h5>
+                </div>
+              </div>
+              <div class="card item" data-avatar="Summer韓小夏" data-img="summer">
+                <div class="imgfr"><img src="static/img/summer.webp" class="card-img-top" alt="..."></div>
+                <div class="card-body">
+                  <h5 class="card-title">Summer韓小夏</h5>
+                </div>
+              </div>
+              <div class="card item" data-avatar="Jocelyn" data-img="Jocelyn">
+                <div class="imgfr"><img src="static/img/Jocelyn.webp" class="card-img-top" alt="..."></div>
+                <div class="card-body">
+                  <h5 class="card-title">Jocelyn</h5>
+                </div>
+              </div>
+              <div class="card item" data-avatar="Angela" data-img="Angela">
+                <div class="imgfr"><img src="static/img/Angela.webp" class="card-img-top" alt="..."></div>
+                <div class="card-body">
+                  <h5 class="card-title">Angela</h5>
+                </div>
+              </div>
+            </div>
+          </fieldset>
+          <fieldset>
+            <h3 class="fs-subtitle" set-lan="html:lines">台詞</h3>
+            <input id=slide_raw_url type="text" name='t1' class='title_new' value="" placeholder="標題" /><label for="myCheck" set-lan="html:add_eng">加入英文:</label> 
+            <input type="checkbox" id="multiLang" > <br/>
+             <div class="subtitle-inputs">
+            
+            </div>
+              <span class="add">+</span>
+          </fieldset>
+          <fieldset id='imgSrc'>
+            <h3 class="fs-subtitle" style="display: inline-block;"><lan set-lan="html:img_link">影像連結</lan><img class="ms-1" src="static/img/question.png" alt="" data-bs-toggle="tooltip" data-bs-placement="right" title="僅接受png, jpg, mp4格式"></h3><br/>
+            <div class="img-inputs">
+            </div>
+            <span class="addimg">+</span>
+            <input id="sendBTN" type="button" name="next" class="next action-button" set-lan="val:submit" value="送出" />
+            <h3 style="display: none;" class="fs-subtitle"><lan set-lan="html:processing_progress">處理進度</lan></h3>
+            <div style="display: none;" id="myProgress">
+              <div style="display: none;" id="myBar">0%</div>
+            </div>
+          </fieldset>
+        </form>
+        <!-- <div style="width: 80%;margin: 0 auto;"><iframe src="http://www.choozmo.com:8168/ai_anchor_video/16250306886652043.mp4" frameborder="0" style="width: 100%;height: 400px;"></iframe></div> -->
+      </div>
+      
+      <div class="modal fade" tabindex="-1" id="howto" aria-labelledby="howto" aria-hidden="true">
+        <div class="modal-dialog">
+          <div class="modal-content">
+            <div class="modal-header">
+              <h5 class="modal-title" id="staticBackdropLabel" set-lan="html:usage_intro">使用說明</h5>
+              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <div class="modal-body">
+                <div class="modal-terms">
+                    <ol class="ps-0">
+                      <li set-lan="html:one_line_to_one_img">1. 一句台詞請對應提供一個影像連結做為搭配</li>
+                      <li><lang set-lan="html:sup_img_profile">2. 影像連結檔案格式支援:</lang><stong class="strong">.png, jpg, .mp4</stong></li>
+                      <li set-lan="html:submit_to_wait">3. 點選“送出”之後需等待一段影片製作的時間,請您耐心等候,待製作完畢可於通知網址查看</li>
+                  </ol>
+                </div>
+            </div>
+          </div>
+        </div>
+    </div> 
+    <div class="modal" tabindex="-1" id="history" aria-labelledby="history" aria-hidden="true">
+      <div class="modal-dialog modal-dialog-scrollable">
+        <div class="modal-content">
+          <div class="modal-header">
+            <h5 class="modal-title" id="staticBackdropLabel" set-lang="html:history">歷史紀錄</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+          </div>
+          <div class="modal-body">
+              <div class="modal-terms">
+                <div class="loader"><img src="static/img/bx_loader.gif" alt=""></div>
+                <ol class="ps-0 historyList">
+                </ol>
+              </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal" tabindex="-1"  id="avatarmega" aria-labelledby="history" aria-hidden="true">
+      <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content text-center">
+          <div class="modal-header">
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"><img src="static/img/close.svg" alt=""></button>
+          </div>
+          <div class="modal-body">
+            <img class="modal-img" src="" alt="">
+            <h5 class="modal-title mt-2"></h5>
+          </div>
+        </div>
+      </div>
+    </div>
+    </div>
+
+    
+  
+
+ {% endblock %}

+ 35 - 6
subGenerator/ProcessSub.py

@@ -47,8 +47,37 @@ for s in adjs:
     print(s)
 '''
 
-#gts = 'Hello Kitty 於2018年,加入YouTube開始活動,在自我介紹的影片裡,Kitty表示一直憧憬著,想在YouTube跟大家見面,一開頻道就吸引許多粉絲訂閱,目前有28萬訂閱者,接下來這位花生君,於2017年加入YouTube開始活動,他的外型太過特別,花生頭、紅色圍巾與紙尿布,被觀眾評價為,第一眼看上很噁心,但看著看著還挺可愛,目前有12萬訂閱者'.split(',')
-#gens = ['Hello Kitty瑜2018年加入YouTube開始活動','再次我介紹的影片裡','Kitty表示一直憧憬著長在YouTube跟大家見面','一開頻道就吸引許多粉絲訂閱付錢有28萬訂閱者','接下來這位花生君瑜2017年加入YouTube開始湖','活動','他的外型太過特別花生桃紅色圍巾魚紙尿布','被觀眾評價為第一眼看上很噁心但看著看著還挺','秦可愛','目前有12萬訂閱者']
+
+def parse_script(file_path):
+    with open(file_path, 'r') as f:
+        lines = [line.strip() for line in f]
+    dict_list = []
+    
+    for idx in range(int((len(lines)+1)/4)):
+        script={}
+        script['index'] = lines[idx * 4]
+        time_raw = lines[idx * 4 + 1]
+        script['content'] = lines[idx * 4 + 2]
+        start = time_raw.split(' --> ')[0].split(':')
+        stop = time_raw.split(' --> ')[1].split(':')
+        start[2] = start[2].replace(',','.')
+        stop[2] = stop[2].replace(',','.')
+        start_sec = float(start[0])*3600 + float(start[1])*60 + float(start[2])
+        stop_sec = float(stop[0])*3600 + float(stop[1])*60 + float(stop[2])
+        duration = start_sec-stop_sec
+        script['start'] = start_sec
+        script['stop'] = stop_sec
+        script['duration'] = abs(duration)
+        dict_list.append(script)
+    return dict_list
+
+gg = parse_script('out.txt')
+for g in gg:
+    print(g) 
+            
+    
+gts = 'Hello Kitty 於2018年,加入YouTube開始活動,在自我介紹的影片裡,Kitty表示一直憧憬著,想在YouTube跟大家見面,一開頻道就吸引許多粉絲訂閱,目前有28萬訂閱者,接下來這位花生君,於2017年加入YouTube開始活動,他的外型太過特別,花生頭、紅色圍巾與紙尿布,被觀眾評價為,第一眼看上很噁心,但看著看著還挺可愛,目前有12萬訂閱者'.split(',')
+gens = ['Hello Kitty瑜2018年加入YouTube開始活動','再次我介紹的影片裡','Kitty表示一直憧憬著長在YouTube跟大家見面','一開頻道就吸引許多粉絲訂閱付錢有28萬訂閱者','接下來這位花生君瑜2017年加入YouTube開始湖','活動','他的外型太過特別花生桃紅色圍巾魚紙尿布','被觀眾評價為第一眼看上很噁心但看著看著還挺','秦可愛','目前有12萬訂閱者']
 def adjustSub_by_text_similarity(gts,gens):
     combine2 = [''.join([i,j]) for i,j in zip(gts, gts[1:])]
     combine3 = [''.join([i,j,k]) for i,j,k in zip(gts, gts[1:], gts[2:])]
@@ -57,8 +86,8 @@ def adjustSub_by_text_similarity(gts,gens):
     for idx in range(len(gens)):
         match_text = difflib.get_close_matches(gens[idx], alls, cutoff=0.1)
         if len(match_text) != 0:
-            #print(gens[idx]+' = '+match_text[0])
+            print('{ '+gens[idx]+' }校正後: '+match_text[0])
             gens[idx] = match_text[0]
-        #else:
-        #   print(gens[idx])
-#adjustSub_by_text_similarity(gts,gens)
+        else:
+           print('無校正:'+gens[idx])
+adjustSub_by_text_similarity(gts,gens)