Browse Source

原始版本

Mike 3 years ago
commit
43c46362ac
100 changed files with 9108 additions and 0 deletions
  1. BIN
      .DS_Store
  2. BIN
      OpenshotService/.DS_Store
  3. BIN
      OpenshotService/__pycache__/openshot_video_generator.cpython-39.pyc
  4. 405 0
      OpenshotService/autosub/__init__-0.4.0.py
  5. 434 0
      OpenshotService/autosub/__init__.py
  6. BIN
      OpenshotService/autosub/__pycache__/__init__.cpython-37.pyc
  7. BIN
      OpenshotService/autosub/__pycache__/constants.cpython-37.pyc
  8. BIN
      OpenshotService/autosub/__pycache__/formatters.cpython-37.pyc
  9. 118 0
      OpenshotService/autosub/constants.py
  10. 66 0
      OpenshotService/autosub/formatters.py
  11. BIN
      OpenshotService/font/DFT_B7.ttc
  12. BIN
      OpenshotService/font/arial.ttf
  13. 596 0
      OpenshotService/font/main.py
  14. BIN
      OpenshotService/font/msjh.ttf
  15. 1005 0
      OpenshotService/openshot_video_generator.py
  16. BIN
      OpenshotService/pytranscriber/.DS_Store
  17. 0 0
      OpenshotService/pytranscriber/control/__init__.py
  18. BIN
      OpenshotService/pytranscriber/control/__pycache__/__init__.cpython-37.pyc
  19. BIN
      OpenshotService/pytranscriber/control/__pycache__/ctr_autosub.cpython-37.pyc
  20. BIN
      OpenshotService/pytranscriber/control/__pycache__/ctr_main.cpython-37.pyc
  21. BIN
      OpenshotService/pytranscriber/control/__pycache__/thread_cancel_autosub.cpython-37.pyc
  22. BIN
      OpenshotService/pytranscriber/control/__pycache__/thread_exec_autosub.cpython-37.pyc
  23. 145 0
      OpenshotService/pytranscriber/control/ctr_autosub.py
  24. 413 0
      OpenshotService/pytranscriber/control/ctr_main.py
  25. 14 0
      OpenshotService/pytranscriber/control/thread_cancel_autosub.py
  26. 120 0
      OpenshotService/pytranscriber/control/thread_exec_autosub.py
  27. 0 0
      OpenshotService/pytranscriber/gui/__init__.py
  28. BIN
      OpenshotService/pytranscriber/gui/__pycache__/__init__.cpython-37.pyc
  29. BIN
      OpenshotService/pytranscriber/gui/__pycache__/gui.cpython-37.pyc
  30. 120 0
      OpenshotService/pytranscriber/gui/gui.py
  31. 266 0
      OpenshotService/pytranscriber/gui/gui.ui
  32. 0 0
      OpenshotService/pytranscriber/model/__init__.py
  33. BIN
      OpenshotService/pytranscriber/model/__pycache__/__init__.cpython-37.pyc
  34. BIN
      OpenshotService/pytranscriber/model/__pycache__/param_autosub.cpython-37.pyc
  35. 22 0
      OpenshotService/pytranscriber/model/param_autosub.py
  36. 0 0
      OpenshotService/pytranscriber/util/__init__.py
  37. BIN
      OpenshotService/pytranscriber/util/__pycache__/__init__.cpython-37.pyc
  38. BIN
      OpenshotService/pytranscriber/util/__pycache__/srtparser.cpython-37.pyc
  39. BIN
      OpenshotService/pytranscriber/util/__pycache__/util.cpython-37.pyc
  40. 49 0
      OpenshotService/pytranscriber/util/srtparser.py
  41. 44 0
      OpenshotService/pytranscriber/util/util.py
  42. 41 0
      OpenshotService/test.py
  43. BIN
      OpenshotService/util/__pycache__/parser.cpython-39.pyc
  44. 57 0
      OpenshotService/util/parser.py
  45. BIN
      api/.DS_Store
  46. BIN
      api/__pycache__/gSlide.cpython-39.pyc
  47. BIN
      api/__pycache__/mailer.cpython-39.pyc
  48. BIN
      api/__pycache__/main.cpython-39.pyc
  49. BIN
      api/__pycache__/models.cpython-39.pyc
  50. 79 0
      api/gSlide.py
  51. 57 0
      api/mailer.py
  52. 709 0
      api/main.py
  53. 58 0
      api/models.py
  54. 12 0
      api/spread2.json
  55. BIN
      api/static/.DS_Store
  56. 110 0
      api/static/gen_avatar.html
  57. 189 0
      api/static/gen_avatar.js
  58. BIN
      api/static/img/Angela.webp
  59. BIN
      api/static/img/Jocelyn.webp
  60. BIN
      api/static/img/Spinner-1s-181px.gif
  61. BIN
      api/static/img/angus.webp
  62. BIN
      api/static/img/bx_loader.gif
  63. 1 0
      api/static/img/close.svg
  64. BIN
      api/static/img/girl2.png
  65. BIN
      api/static/img/ninablack.webp
  66. BIN
      api/static/img/ninawhite.webp
  67. BIN
      api/static/img/peggy.webp
  68. BIN
      api/static/img/question.png
  69. BIN
      api/static/img/stacy.webp
  70. BIN
      api/static/img/summer.webp
  71. 0 0
      api/static/img/undraw_male_avatar_323b.svg
  72. 0 0
      api/static/img/undraw_mobile_user_7oqo.svg
  73. 0 0
      api/static/img/undraw_video_upload_3d4u.svg
  74. BIN
      api/static/img/wave.png
  75. 240 0
      api/static/index2.html
  76. 248 0
      api/static/index_eng.html
  77. 164 0
      api/static/lan.js
  78. 5 0
      api/static/owl.carousel.min.css
  79. 5 0
      api/static/owl.carousel.min.js
  80. 6 0
      api/static/owl.theme.default.min.css
  81. 241 0
      api/static/script_anchor_eng.js
  82. 471 0
      api/static/script_util.js
  83. 925 0
      api/static/style.css
  84. 107 0
      api/templates/index.html
  85. 92 0
      api/templates/login.html
  86. 170 0
      api/templates/make_video.html
  87. 170 0
      api/templates/make_video_long.html
  88. 149 0
      api/templates/make_video_slide.html
  89. 59 0
      api/templates/privacy.html
  90. 54 0
      api/templates/script_index.js
  91. 22 0
      api/templates/user_profile.html
  92. BIN
      api/util/__pycache__/swap_face.cpython-39.pyc
  93. BIN
      api/util/__pycache__/user.cpython-39.pyc
  94. 51 0
      api/util/swap_face.py
  95. 58 0
      api/util/user.py
  96. 26 0
      etc/Dockerfile
  97. 10 0
      etc/docker cmd.txt
  98. 595 0
      etc/main.py
  99. 14 0
      subGenerator/.gitignore
  100. 96 0
      subGenerator/ProcessSub.py

BIN
.DS_Store


BIN
OpenshotService/.DS_Store


BIN
OpenshotService/__pycache__/openshot_video_generator.cpython-39.pyc


+ 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,
+}

BIN
OpenshotService/font/DFT_B7.ttc


BIN
OpenshotService/font/arial.ttf


+ 596 - 0
OpenshotService/font/main.py

@@ -0,0 +1,596 @@
+from fastapi import FastAPI,Cookie, Depends, FastAPI, Query, WebSocket, status, WebSocketDisconnect
+import openshot
+from os import listdir
+from os.path import isfile, isdir, join
+import threading
+import zhtts
+import os 
+import urllib
+from typing import List
+import requests
+from pydantic import BaseModel
+from bs4 import BeautifulSoup
+from PIL import Image,ImageDraw,ImageFont
+import pyttsx3
+import rpyc
+import random
+import time
+import math
+import hashlib
+import re
+import asyncio
+import urllib.request
+from fastapi.responses import FileResponse
+from websocket import create_connection
+from fastapi.middleware.cors import CORSMiddleware
+import dataset
+from datetime import datetime
+from util.swap_face import swap_face
+from fastapi.staticfiles import StaticFiles
+#service nginx restart 
+#uvicorn main:app --host="0.0.0.0" --reload --port 8888
+
+app = FastAPI()
+origins = [
+    "https://hhh.com.tw"
+    "http://172.105.205.52",
+    "http://172.105.205.52:8001",
+    "http://172.104.93.163",
+]
+
+app.add_middleware(
+    CORSMiddleware,
+    # allow_origins=origins,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+app.mount("/static", StaticFiles(directory="static"), name="static")
+app.mount("/static/img", StaticFiles(directory="static/img"), name="static/img")
+
+dir_sound = 'mp3_track/'
+dir_photo = 'photo/'
+dir_text = 'text_file/'
+dir_video = 'video_material/'
+dir_title = 'title/'
+dir_subtitle = 'subtitle/'
+dir_anchor = 'anchor_raw/'
+
+class swap_req(BaseModel):
+    imgurl: str
+
+class request(BaseModel):
+    name: str
+    text_content: List[str]
+    image_urls: List[str]
+    avatar: str
+    client_id :str
+
+
+class ConnectionManager:
+    def __init__(self):
+        self.active_connections: List[WebSocket] = []
+
+    async def connect(self, websocket: WebSocket):
+        await websocket.accept()
+        self.active_connections.append(websocket)
+
+    def disconnect(self, websocket: WebSocket):
+        self.active_connections.remove(websocket)
+
+    async def send_personal_message(self, message: str, websocket: WebSocket):
+        await websocket.send_text(message)
+
+    async def broadcast(self, message: str):
+        for connection in self.active_connections:
+            await connection.send_text(message)
+
+
+
+
+@app.get("/")
+async def root():
+    return {"message": "Hello, this is index"}
+
+@app.get("/index2")
+async def index2():
+    return FileResponse('static/index2.html')
+
+@app.get("/gen_avatar")
+async def index2():
+    return FileResponse('gen_avatar.html')
+
+@app.post("/swapFace")
+async def swapFace(req:swap_req):
+    sf = swap_face(req.imgurl)
+    result = sf.run()
+    #notify_group(result)hi
+    return result
+
+
+@app.post("/make_anchor_video_v2")
+async def make_anchor_video_v2(req:request):
+    for txt in req.text_content:
+        if re.search('[a-zA-Z]', txt) !=None:
+            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)
+                f=open(dir_photo+name_hash+"/"+str(img_num)+".mp4",'wb')
+            else:
+                im = Image.open(requests.get(imgu, stream=True).raw)
+                im= im.convert("RGB")
+        except:
+            return {'msg':"無法辨別圖片網址"+imgu}
+    
+    save_history(req,name_hash)
+    x = threading.Thread(target=anchor_video_v2, args=(name_hash,req.name, req.text_content, req.image_urls,int(req.avatar),req.client_id))
+    x.start()
+    return {"msg":"製作影片需要時間,請您耐心等候  稍後可以在www.choozmo.com:8168/"+name_hash+".mp4 中觀看"} 
+
+manager = ConnectionManager()
+@app.websocket("/progress/{client_id}")
+async def websocket_endpoint(websocket: WebSocket, client_id: int):
+    await manager.connect(websocket)
+    try:
+        while True:
+            data = await websocket.receive_text()
+            await manager.send_personal_message(data, websocket)
+            await manager.broadcast(data)
+    except WebSocketDisconnect:
+        manager.disconnect(websocket)
+        
+
+@app.get("/history_input")
+async def history_input():
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    statement = 'SELECT * FROM history_input ORDER BY timestamp DESC LIMIT 50'
+    logs = []
+    for row in db.query(statement):
+        logs.append({'id':row['id'],'name':row['name'],'text_content':row['text_content'].split(','),'link':row['link'],'image_urls':row['image_urls'].split(',')})
+    return logs
+
+def notify_group(msg):
+    glist=['7vilzohcyQMPLfAMRloUawiTV4vtusZhxv8Czo7AJX8','WekCRfnAirSiSxALiD6gcm0B56EejsoK89zFbIaiZQD','1dbtJHbWVbrooXmQqc4r8OyRWDryjD4TMJ6DiDsdgsX']
+    for gid in glist:
+        headers = {
+                "Authorization": "Bearer " + gid,
+                "Content-Type": "application/x-www-form-urlencoded"
+        }
+        params = {"message": msg}   
+        r = requests.post("https://notify-api.line.me/api/notify",headers=headers, params=params)
+
+
+def save_history(req,name_hash):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    log_table = db['history_input']
+    txt_content_seperate_by_dot = ''
+    for txt in req.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 req.image_urls:
+        img_urls_seperate_by_dot += iurl+","
+    img_urls_seperate_by_dot = img_urls_seperate_by_dot[:-1]
+    time_stamp = datetime.fromtimestamp(time.time())
+    time_stamp = time_stamp.strftime("%Y-%m-%d %H:%M:%S")
+    pk = log_table.insert({'name':req.name,'text_content':txt_content_seperate_by_dot,'image_urls':img_urls_seperate_by_dot,'link':'www.choozmo.com:8168/'+name_hash+'.mp4','timestamp':time_stamp})
+    
+
+def cKey(r,g,b,fuzz):
+    col=openshot.Color()
+    col.red=openshot.Keyframe(r)
+    col.green=openshot.Keyframe(g)
+    col.blue=openshot.Keyframe(b)
+    return openshot.ChromaKey(col, openshot.Keyframe(fuzz))
+
+def video_photo_clip(vid=None,layer=None, position=None, end=None
+    ,scale_x=1,scale_y=1,location_x=0,location_y=0,ck=None,audio=True):
+    clip = openshot.Clip(vid)
+    clip.Layer(layer)
+    clip.Position(position)
+    clip.End(end)
+    clip.scale_x=openshot.Keyframe(scale_x)
+    clip.scale_y=openshot.Keyframe(scale_y)
+    clip.location_x=openshot.Keyframe(location_x)
+    clip.location_y=openshot.Keyframe(location_y)
+    
+    if ck!=None:
+        clip.AddEffect(ck)
+    if audio==True:
+        clip.has_audio=openshot.Keyframe(1)
+    else:
+        clip.has_audio=openshot.Keyframe(0)
+    return clip
+
+
+
+
+def myunichchar(unicode_char):
+        mb_string = unicode_char.encode('big5')
+        try:
+            unicode_char = unichr(ord(mb_string[0]) << 8 | ord(mb_string[1]))
+        except NameError:
+            unicode_char = chr(mb_string[0] << 8 | mb_string[1])
+        return unicode_char
+
+
+def file_prepare(name, name_hash,text_content,image_urls):
+    #save image
+    try:
+        os.mkdir(dir_photo+name_hash)
+    except FileExistsError:
+        print("Directory " , dir_photo+name_hash ,  " already exists")
+    img_num = 1
+    for imgu in image_urls:
+        im = Image.open(requests.get(imgu, stream=True).raw)
+        im.save(dir_photo+name_hash+"/"+str(img_num)+".jpg")
+        img_num+=1
+    #save text
+    text_file = open(dir_text+name_hash+".txt", "w")
+    text_file.write(text_content)
+    text_file.close()
+    print("text file made")
+    #make mp3
+    tts = zhtts.TTS() 
+    tts.text2wav(text_content,dir_sound+name_hash+".mp3")
+    print("mp3 file made")
+    #make title as image
+    txt2image(name, dir_title+name_hash+".png")
+
+def get_url_type(url):
+    req = urllib.request.Request(url, method='HEAD', headers={'User-Agent': 'Mozilla/5.0'})
+    r = urllib.request.urlopen(req)
+    contentType = r.getheader('Content-Type')
+    return contentType
+    
+def downloadfile(name,url):
+    name=name+".mp4"
+    
+def make_dir(name_hash):
+    #save image
+    try:
+        os.mkdir(dir_photo+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_photo+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_text+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_text+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_sound+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_sound+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_video+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_video+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_anchor+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_anchor+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_subtitle+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_subtitle+name_hash ,  " already exists")
+
+def file_prepare_v2(name, name_hash,text_content,image_urls):
+    make_dir(name_hash)
+    img_num = 1
+    for imgu in image_urls:
+        if get_url_type(imgu) =='video/mp4':
+            r=requests.get(imgu)
+            f=open(dir_photo+name_hash+"/"+str(img_num)+".mp4",'wb')
+            for chunk in r.iter_content(chunk_size=255): 
+                if chunk:
+                    f.write(chunk)
+            f.close()
+        else:
+            im = Image.open(requests.get(imgu, stream=True).raw)
+            im= im.convert("RGB")
+            im.save(dir_photo+name_hash+"/"+str(img_num)+".jpg")
+        img_num+=1
+    #save text
+    txt_idx=0
+    for txt in text_content:
+        text_file = open(dir_text+name_hash+"/"+str(txt_idx)+".txt", "w")
+        text_file.write(txt)
+        text_file.close()
+        txt_idx+=1
+    print("text file made")
+    #make mp3
+    language = 'zh-tw'
+    txt_idx = 0
+    for txt in text_content:
+        tts = zhtts.TTS() 
+        tts.text2wav(txt,dir_sound+name_hash+"/"+str(txt_idx)+".mp3")
+        txt_idx+=1
+    print("mp3 file made")
+    #make title as image
+    txt2image_title(name, dir_title+name_hash+".png")
+
+def txt2image(content, save_target):
+    unicode_text = trim_punctuation(content)
+    font = ImageFont.truetype(font="DFT_B7.ttc", size=38)
+    text_width, text_height = font.getsize(unicode_text)
+    canvas = Image.new('RGBA', (700, 500), (255, 0, 0, 0) )
+    draw = ImageDraw.Draw(canvas)
+    text= unicode_text
+    draw.text((5,5), text, (255, 255, 0), font)
+    canvas.save(save_target, "PNG")
+def txt2image_title(content, save_target):
+    unicode_text = trim_punctuation(content)
+    font = ImageFont.truetype(font="DFT_B7.ttc", size=28)
+    text_width, text_height = font.getsize(unicode_text)
+    canvas = Image.new('RGBA', (510, 500), (255, 0, 0, 0) )
+    draw = ImageDraw.Draw(canvas)
+    text= unicode_text
+    draw.text((5,5), text, (17, 41, 167), font)
+    canvas.save(save_target, "PNG")
+'''
+def txt2image_title(content, save_target):
+    unicode_text =content
+    font = ImageFont.truetype("font.ttf", 23,encoding='big5')
+    text_width, text_height = font.getsize(unicode_text)
+    canvas = Image.new('RGBA', (500, 500), (255, 0, 0, 0) )
+    draw = ImageDraw.Draw(canvas)
+    text=''
+    for c in unicode_text:
+        if len(re.findall(r'[\u4e00-\u9fff]+', c))>0:
+            text+=myunichchar(c)
+        else:
+            text+=c
+    draw.text((5,5), text, (17, 41, 167), font)
+    canvas.save(save_target, "PNG")
+'''
+def call_anchor(fileName,avatar):
+    conn = rpyc.classic.connect("192.168.1.105",18812)
+    ros = conn.modules.os 
+    rsys = conn.modules.sys 
+    fr=open(dir_sound+fileName+".mp3",'rb')# voice
+    #warning!!!    file my be replaced by other process
+    fw=conn.builtins.open('/tmp/output.mp3','wb')
+
+    while True:
+        b=fr.read(1024)
+        if b:
+            fw.write(b)
+        else:
+            break
+
+    fr.close()
+    fw.close()
+
+    val=random.randint(1000000,9999999)
+    ros.chdir('/home/jared/to_video')
+    ros.system('./p'+str(avatar)+'.sh '+str(val)+' &')
+
+    while True:
+        print('waiting...')
+        if ros.path.exists('/tmp/results/'+str(val)):
+            break
+        time.sleep(5)
+        print('waiting...')
+
+    fr=conn.builtins.open('/tmp/results/'+str(val)+'.mp4','rb')
+    fw=open(dir_anchor+fileName+".mp4",'wb')#peggy1_1
+    while True:
+        b=fr.read(1024)
+        if b:
+            fw.write(b)
+        else:
+            break
+
+    fr.close()
+    fw.close()
+
+
+
+def trim_punctuation(s):
+    pat_block = u'[^\u4e00-\u9fff0-9a-zA-Z]+';
+    pattern = u'([0-9]+{0}[0-9]+)|{0}'.format(pat_block)
+    res = re.sub(pattern, lambda x: x.group(1) if x.group(1) else u"" ,s)
+    return res
+
+def splitter(s):
+    for sent in re.findall(u'[^!?,。\!\?]+[!?。\!\?]?', s, flags=re.U):
+        yield sent
+
+def split_by_pun(s):
+    res = list(splitter(s))
+    return res
+
+def generate_subtitle_image(name_hash,text_content):
+    img_list = [None]*len(text_content)
+    for idx in range(len(text_content)):
+        img_list[idx]=[]
+        senList = split_by_pun(text_content[idx])
+        for inner_idx in range(len(senList)):
+            sv_path = dir_subtitle + name_hash +'/'+str(idx)+ str(inner_idx) +'.png'
+            sub = senList[inner_idx]
+            txt2image(sub,sv_path)
+            img_list[idx]+=[{"count":len(sub),"path":sv_path}]
+    return img_list
+
+async def sendProgress(progress,client_id):
+    ws = create_connection("ws://www.choozmo.com:8888/progress/"+client_id)
+    ws.send(str(progress))
+    ws.close()
+
+def anchor_video_v2(name_hash,name,text_content, image_urls,avatar,client_id):
+    
+    progress = 0
+    asyncio.run(sendProgress(progress,client_id))
+    
+    
+    print('sub image made')
+    file_prepare_v2(name, name_hash, text_content,image_urls)
+    progress = 20
+    asyncio.run(sendProgress(progress,client_id))
+    sub_list=generate_subtitle_image(name_hash,text_content)
+    progress = 30
+    asyncio.run(sendProgress(progress,client_id))
+    
+    progress_per_video = int(40/len(text_content))
+    for fname in range(len(text_content)):
+        call_anchor(name_hash+"/"+str(fname),avatar)
+        progress += progress_per_video
+        print('step finish')
+        asyncio.run(sendProgress(progress,client_id))
+    print('called............................................')
+
+    ck=cKey(0,254,0,270)
+    ck_anchor=cKey(0,255,1,320)
+    duration = 0
+    #average layer level is 3
+    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.mp4")
+    LOGO_OP.Open()         # Open the reader
+    LOGO_OP_clip = video_photo_clip(vid=LOGO_OP,layer=4,position=0,end=LOGO_OP.info.duration
+                    ,location_y=-0.03,scale_x=0.8,scale_y=0.704)
+    t.AddClip(LOGO_OP_clip)
+    bg_head = openshot.FFmpegReader(dir_video+"bg_head.avi")
+    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 += LOGO_OP.info.duration
+    head_duration = LOGO_OP.info.duration
+    bg_head.Close()
+    LOGO_OP.Close()
+    progress += 10
+    
+
+    
+    clip_duration=0
+    photo_clip_list = [None]*len(text_content)
+    img_list = [None]*len(text_content)
+    anchor_clip_list = [None] * len(text_content)
+    anchor_list = [None] * len(text_content)
+    audio_clip_list = [None] * len(text_content)
+    audio_list = [None] * len(text_content)
+    sub_clip_list = [None] * len(text_content)
+    sub_img_list = [None] * len(text_content)
+    
+    idx = 0
+    for p in listdir(dir_photo+name_hash):
+        
+        anchor_list[idx] = openshot.FFmpegReader(dir_anchor+name_hash+"/"+str(idx)+".mp4")
+        clip_duration = anchor_list[idx].info.duration
+        anchor_list[idx].Open()
+        anchor_clip_list[idx] = video_photo_clip(vid=anchor_list[idx],layer=4,scale_x=0.65,scale_y=0.65,
+                location_x=0.35,location_y=0.25,position=main_timer, end=clip_duration,ck=ck_anchor,audio=False)
+        t.AddClip(anchor_clip_list[idx])
+
+        img_list[idx] = openshot.FFmpegReader(dir_photo+name_hash+'/'+p)
+        img_list[idx].Open()
+        photo_clip_list[idx] = video_photo_clip(vid=img_list[idx],layer=3
+                ,scale_x=0.81,scale_y=0.68,location_y=-0.03,position=main_timer,end=clip_duration,audio=False)
+        t.AddClip(photo_clip_list[idx])
+        img_list[idx].Close()
+
+        audio_list[idx] = openshot.FFmpegReader(dir_sound+name_hash+"/"+str(idx)+".mp3")
+        audio_list[idx].Open()
+        audio_clip_list[idx] = openshot.Clip(audio_list[idx])
+        audio_clip_list[idx].Position(main_timer)
+        audio_clip_list[idx].End(clip_duration)
+        t.AddClip(audio_clip_list[idx])
+
+        img_list[idx].Close()
+        anchor_list[idx].Close()
+        audio_list[idx].Close()
+
+     
+            
+        sub_img_list[idx] = [None] * len(sub_list[idx])
+        sub_clip_list[idx] = [None] * len(sub_list[idx])
+        sub_timer = 0
+        for sub_idx in range(len(sub_list[idx])):
+            sub_img_list[idx][sub_idx] = openshot.QtImageReader(sub_list[idx][sub_idx]['path'])
+            sub_img_list[idx][sub_idx].Open()
+            sub_duration = 0.205*sub_list[idx][sub_idx]['count']
+            sub_clip_list[idx][sub_idx] = video_photo_clip(vid=sub_img_list[idx][sub_idx], layer=6,location_x=0.069, location_y=0.89,position=main_timer+sub_timer,end=sub_duration)
+            t.AddClip(sub_clip_list[idx][sub_idx])
+            sub_img_list[idx][sub_idx].Close()
+            sub_timer += sub_duration
+            print(sub_list[idx][sub_idx]['path'])
+        main_timer += clip_duration
+        idx+=1
+
+    progress+=10
+    asyncio.run(sendProgress(progress,client_id))
+    
+    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+2
+                    ,location_x=0.005,location_y=-0.031
+                    ,scale_x=0.8,scale_y=0.6825)
+    t.AddClip(LOGO_ED_clip)
+    ED_duration = LOGO_ED.info.duration
+    LOGO_ED.Close()
+    
+
+    bg = openshot.FFmpegReader(dir_video+"bg.mp4")
+    bg.Open()
+    bg_times = math.floor(main_timer+ED_duration/bg.info.duration)
+    left_time = (main_timer+ED_duration) % 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+"bg.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+"bg.mp4")
+    bg_left.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)
+
+    ####start building
+    w = openshot.FFmpegWriter("../html/"+name_hash+".mp4")
+    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)
+    w.Open()
+    
+    #may change duration into t.info.duration
+    frames = int(t.info.fps)*int(head_duration+main_timer+ED_duration)
+    for n in range(frames):
+        f=t.GetFrame(n)
+        w.WriteFrame(f)
+        
+            
+    progress = 100
+    asyncio.run(sendProgress(progress,client_id))
+    notify_group(name+"的影片已經產生完成囉! www.choozmo.com:8168/"+name_hash+".mp4")
+    t.Close()
+    w.Close()
+
+
+    progress = 100
+    asyncio.run(sendProgress(progress,client_id))
+    print("Raw Video done")
+    print("video at : www.choozmo.com:8168/"+name_hash+".mp4")
+
+    #line notifs
+    
+
+

BIN
OpenshotService/font/msjh.ttf


+ 1005 - 0
OpenshotService/openshot_video_generator.py

@@ -0,0 +1,1005 @@
+from os import listdir
+from os.path import isfile, isdir, join
+import openshot
+import threading
+import zhtts
+import os 
+import urllib
+from typing import List
+import requests
+from pydantic import BaseModel
+from bs4 import BeautifulSoup
+from PIL import Image,ImageDraw,ImageFont
+import pyttsx3
+import rpyc
+import random
+import re
+import time
+import math
+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
+from itertools import groupby
+from operator import itemgetter
+from util.parser import parser
+
+dir_sound = 'mp3_track/'
+dir_photo = 'photo/'
+dir_text = 'text_file/'
+dir_video = 'video_material/'
+dir_title = 'title/'
+dir_subtitle = 'subtitle/'
+dir_anchor = 'anchor_raw/'
+tmp_video_dir = 'tmp_video/'
+video_sub_folder = 'ai_anchor_video/'
+
+dir_list = [dir_sound,dir_photo,dir_text,dir_video,dir_title,dir_subtitle,dir_anchor,tmp_video_dir]
+
+def notify_group(msg):
+    glist=['7vilzohcyQMPLfAMRloUawiTV4vtusZhxv8Czo7AJX8','WekCRfnAirSiSxALiD6gcm0B56EejsoK89zFbIaiZQD','1dbtJHbWVbrooXmQqc4r8OyRWDryjD4TMJ6DiDsdgsX','HOB1kVNgIb81tTB4Ort1BfhVp9GFo6NlToMQg88vEhh']
+    for gid in glist:
+        headers = {
+                "Authorization": "Bearer " + gid,
+                "Content-Type": "application/x-www-form-urlencoded"
+        }
+        params = {"message": msg}   
+        r = requests.post("https://notify-api.line.me/api/notify",headers=headers, params=params)
+
+def cKey(r,g,b,fuzz):
+    col=openshot.Color()
+    col.red=openshot.Keyframe(r)
+    col.green=openshot.Keyframe(g)
+    col.blue=openshot.Keyframe(b)
+    return openshot.ChromaKey(col, openshot.Keyframe(fuzz))
+
+def video_photo_clip(vid=None,layer=None, position=None, end=None
+    ,scale_x=1,scale_y=1,location_x=0,location_y=0,ck=None,audio=True):
+    clip = openshot.Clip(vid)
+    clip.Layer(layer)
+    clip.Position(position)
+    clip.End(end)
+    clip.scale_x=openshot.Keyframe(scale_x)
+    clip.scale_y=openshot.Keyframe(scale_y)
+    clip.location_x=openshot.Keyframe(location_x)
+    clip.location_y=openshot.Keyframe(location_y)
+    
+    if ck!=None:
+        clip.AddEffect(ck)
+    if audio==True:
+        clip.has_audio=openshot.Keyframe(1)
+    else:
+        clip.has_audio=openshot.Keyframe(0)
+    return clip
+
+def listener_progress(string, percent):
+    True
+
+def myunichchar(unicode_char):
+        mb_string = unicode_char.encode('big5')
+        try:
+            unicode_char = unichr(ord(mb_string[0]) << 8 | ord(mb_string[1]))
+        except NameError:
+            unicode_char = chr(mb_string[0] << 8 | mb_string[1])
+        return unicode_char
+
+def get_url_type(url):
+    req = urllib.request.Request(url, method='HEAD', headers={'User-Agent': 'Mozilla/5.0'})
+    r = urllib.request.urlopen(req)
+    contentType = r.getheader('Content-Type')
+    return contentType
+    
+def make_dir(name_hash):
+    for direct in dir_list:
+        if not os.path.isdir(direct):
+            os.mkdir(direct)
+    try:
+        os.mkdir(dir_photo+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_photo+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_text+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_text+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_sound+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_sound+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_anchor+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_anchor+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_subtitle+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_subtitle+name_hash ,  " already exists")
+
+def file_prepare(name, name_hash,text_content,image_urls,multiLang,lang='zh'):
+    make_dir(name_hash)
+    img_num = 1
+    for imgu in image_urls:
+        if get_url_type(imgu) =='video/mp4':
+            r=requests.get(imgu)
+            f=open(dir_photo+name_hash+"/"+str(img_num)+".mp4",'wb')
+            for chunk in r.iter_content(chunk_size=255): 
+                if chunk:
+                    f.write(chunk)
+            f.close()
+        else:
+            im = Image.open(requests.get(imgu, stream=True).raw)
+            im= im.convert("RGB")
+            im.save(dir_photo+name_hash+"/"+str(img_num)+".jpg")
+        img_num+=1
+    #save text
+    txt_idx=0
+    for txt in text_content:
+        text_file = open(dir_text+name_hash+"/"+str(txt_idx)+".txt", "w")
+        text_file.write(txt)
+        text_file.close()
+        txt_idx+=1
+    print("text file made")
+    #make mp3
+    txt_idx = 0
+    for txt in text_content:
+        if lang!='zh' or multiLang==1:
+            if lang!='zh':
+                tts = gTTS(txt)
+                tts.save(dir_sound+name_hash+"/"+str(txt_idx)+"raw.mp3")
+            else:
+                tts = gTTS(txt,lang='zh-tw')
+                tts.save(dir_sound+name_hash+"/"+str(txt_idx)+"raw.mp3")
+            #speed up 
+            ff = ffmpy.FFmpeg(inputs={dir_sound+name_hash+"/"+str(txt_idx)+"raw.mp3": None}
+                            , outputs={dir_sound+name_hash+"/"+str(txt_idx)+".mp3": ["-filter:a", "atempo=1.2"]})
+            ff.run()
+            os.remove(dir_sound+name_hash+"/"+str(txt_idx)+"raw.mp3")
+        else:
+            print('use zhtts')
+            tts = zhtts.TTS() 
+            tts.text2wav(txt,dir_sound+name_hash+"/"+str(txt_idx)+".mp3")
+        txt_idx+=1
+    print("mp3 file made")
+    #make title as image
+    txt2image_title(name, dir_title+name_hash+".png",lang)
+
+
+def file_prepare_long(name, name_hash,text_content,image_urls,multiLang,lang='zh'):
+    make_dir(name_hash)
+    img_num = 1
+    for imgu in image_urls:
+        if get_url_type(imgu) =='video/mp4':
+            r=requests.get(imgu)
+            f=open(dir_photo+name_hash+"/"+str(img_num)+".mp4",'wb')
+            for chunk in r.iter_content(chunk_size=255): 
+                if chunk:
+                    f.write(chunk)
+            f.close()
+        else:
+            im = Image.open(requests.get(imgu, stream=True).raw)
+            im= im.convert("RGB")
+            im.save(dir_photo+name_hash+"/"+str(img_num)+".jpg")
+        img_num+=1
+
+    #make mp3
+    text_parser = parser()
+    txt_idx = 0
+    for txt in text_content:
+        rep_list = text_parser.replace_list(txt)
+        for reptxt in rep_list:
+            txt = txt.replace(reptxt,'')
+        if lang!='zh' or multiLang==1:
+            if lang!='zh':
+                tts = gTTS(txt)
+                tts.save(dir_sound+name_hash+"/"+str(txt_idx)+"raw.mp3")
+            else:
+                tts = gTTS(txt,lang='zh-tw')
+                tts.save(dir_sound+name_hash+"/"+str(txt_idx)+"raw.mp3")
+            #speed up 
+            ff = ffmpy.FFmpeg(inputs={dir_sound+name_hash+"/"+str(txt_idx)+"raw.mp3": None}
+                            , outputs={dir_sound+name_hash+"/"+str(txt_idx)+".mp3": ["-filter:a", "atempo=1.2"]})
+            ff.run()
+            os.remove(dir_sound+name_hash+"/"+str(txt_idx)+"raw.mp3")
+        else:
+            print('use zhtts')
+            tts = zhtts.TTS() 
+            tts.text2wav(txt,dir_sound+name_hash+"/"+str(txt_idx)+".mp3")
+        txt_idx+=1
+    print("mp3 file made")
+    #make title as image
+    txt2image_title(name, dir_title+name_hash+".png",lang)
+
+def txt2image(content, save_target,lang='zh'):
+    unicode_text = trim_punctuation(content)
+    font = ''
+    if lang=='zh':
+        font = ImageFont.truetype(font="font/DFT_B7.ttc", size=38)
+    else :
+        font = ImageFont.truetype(font="font/arial.ttf", size=38)
+    text_width, text_height = font.getsize(unicode_text)
+    canvas = Image.new('RGBA', (700, 500), (255, 0, 0, 0) )
+    draw = ImageDraw.Draw(canvas)
+    text= unicode_text
+    draw.text((5,5), text, (255, 255, 0), font)
+    canvas.save(save_target, "PNG")
+
+def txt2image_title(content, save_target, lang='zh'):
+    unicode_text = trim_punctuation(content)
+    font = ''
+    if lang=='zh':
+        font = ImageFont.truetype(font="font/DFT_B7.ttc", size=22)
+    else :
+        font = ImageFont.truetype(font="font/arial.ttf", size=22)
+    text_width, text_height = font.getsize(unicode_text)
+    canvas = Image.new('RGBA', (510, 500), (255, 0, 0, 0) )
+    draw = ImageDraw.Draw(canvas)
+    text= unicode_text
+    draw.text((5,5), text, (17, 41, 167), font)
+    canvas.save(save_target, "PNG")
+
+def call_anchor(fileName,avatar):
+    conn = rpyc.classic.connect("192.168.1.105",18812)
+    ros = conn.modules.os 
+    rsys = conn.modules.sys 
+    fr=open(dir_sound+fileName+".mp3",'rb')# voice
+    #warning!!!    file my be replaced by other process
+    fw=conn.builtins.open('/tmp/output.mp3','wb')
+
+    while True:
+        b=fr.read(1024)
+        if b:
+            fw.write(b)
+        else:
+            break
+
+    fr.close()
+    fw.close()
+
+    val=random.randint(1000000,9999999)
+    ros.chdir('/home/jared/to_video')
+    ros.system('./p'+str(avatar)+'.sh '+str(val)+' &')
+
+    while True:
+        print('waiting...')
+        if ros.path.exists('/tmp/results/'+str(val)):
+            break
+        time.sleep(5)
+        print('waiting...')
+
+    fr=conn.builtins.open('/tmp/results/'+str(val)+'.mp4','rb')
+    fw=open(dir_anchor+fileName+".mp4",'wb')
+    while True:
+        b=fr.read(1024)
+        if b:
+            fw.write(b)
+        else:
+            break
+
+    fr.close()
+    fw.close()
+    
+def syllable_count(word):
+    word = word.lower()
+    count = 0
+    vowels = "aeiouy"
+    if word[0] in vowels:
+        count += 1
+    for index in range(1, len(word)):
+        if word[index] in vowels and word[index - 1] not in vowels:
+            count += 1
+
+    if word.endswith("e"):
+        count -= 1
+    if count == 0:
+        count += 1
+    return count
+
+def split_sentence(in_str, maxLen):
+    re.findall(r'[\u4e00-\u9fff]+', in_str)
+
+    zh_idx = []
+    eng_idx= []
+    for i in range(len(in_str)):
+        if in_str[i] > u'\u4e00' and in_str[i] < u'\u9fff':
+            zh_idx.append(i)
+        else:
+            eng_idx.append(i)
+
+    space_index = [m.start() for m in re.finditer(' ', in_str)]
+    for idx in space_index:
+        eng_idx.remove(idx)
+    
+    eng_range_list = []
+    for k, g in groupby(enumerate(eng_idx), lambda ix : ix[0] - ix[1]):
+        eng_range = list(map(itemgetter(1), g))
+        eng_range_list.append(eng_range)
+
+    total_syllable = 0
+    for i in range(len(eng_range_list)):
+        total_syllable += (syllable_count(in_str[eng_range_list[i][0]:eng_range_list[i][-1]+1])+0.5)
+    for i in range(len(zh_idx)):
+        total_syllable+=1
+    
+    #final chchchchchc[en][en][en]
+    #[en] is a vocabulary dict with  occurence of image
+    zh_eng_idx_list = []
+    i = 0
+    while i < len(in_str):
+        if in_str[i]==' ':
+            i+=1
+        if i in zh_idx:
+            zh_eng_idx_list.append(i)
+            i+=1
+        if i in eng_idx:
+            for ls in eng_range_list:
+                if i in ls:
+                    zh_eng_idx_list.append(ls)
+                    i = ls[-1]+1
+                    break
+            
+    zh_eng_dict_list = [{'content':'','time_ratio':0}]
+    idx = 0 
+    current_len = 0
+    sen_idx = 0
+    while idx < len(zh_eng_idx_list):
+        str_from_idx = ''
+        sylla_cnt = 1
+        if type(zh_eng_idx_list[idx])==type([]):
+            str_from_idx = in_str[zh_eng_idx_list[idx][0]:zh_eng_idx_list[idx][-1]+1]+' '
+            sylla_cnt = syllable_count(str_from_idx)
+        else:
+            str_from_idx = in_str[zh_eng_idx_list[idx]]
+    
+      
+        if len(zh_eng_dict_list[sen_idx]['content'])+sylla_cnt>=maxLen:
+            zh_eng_dict_list[sen_idx]['time_ratio'] = current_len/total_syllable
+           
+            zh_eng_dict_list.append({'content':'','time_ratio':0})
+            sen_idx+=1
+            current_len = 0
+        else:
+            current_len += sylla_cnt
+            zh_eng_dict_list[sen_idx]['content'] += str_from_idx
+            idx+=1
+        
+    total_ratio = 0
+    for obj in zh_eng_dict_list:
+        total_ratio+=obj['time_ratio']
+    zh_eng_dict_list[-1]['time_ratio'] = 1-total_ratio
+    return zh_eng_dict_list
+   
+def parse_script(file_path,gt_list):
+    with open(file_path, 'r',encoding="utf-8") as f:
+        raw_lines = [line.strip() for line in f]
+    lines = adjustSub_by_text_similarity(gt_list,raw_lines)
+    text_parser = parser()
+    #make dict
+    dict_list = []
+    for idx in range(len(lines)):
+        script={}
+        print(lines[idx])
+        rep_ls = text_parser.replace_list(lines[idx])
+        print(rep_ls)
+        line_content = lines[idx]
+        for reptxt in rep_ls:
+            line_content = line_content.replace(reptxt,'')
+        if len(rep_ls)!=0:
+            script['image_idx'] = int(rep_ls[0].replace('{','').replace('}',''))
+        script['content'] = line_content
+        time_raw = raw_lines[idx * 4 +1 ].split(' --> ')
+        start = time_raw[0].split(':')
+        stop = time_raw[1].split(':')
+        script['start'] = float(start[0])*3600 + float(start[1])*60 + float(start[2].replace(',','.'))
+        script['stop'] = float(stop[0])*3600 + float(stop[1])*60 + float(stop[2].replace(',','.'))
+        dict_list.append(script)
+    #merge duplicated sentences
+    script_not_dup_list = []
+    for idx in range(len(dict_list)):
+        dup_list = []
+        for idx_inner in range(len(dict_list)):
+            if dict_list[idx_inner]['content']==dict_list[idx]['content']:
+                dup_list.append(idx_inner)
+        for dup_idx in dup_list:
+            if dup_idx == min(dup_list):
+                dict_list[dup_idx]['type'] = 'lead_sentence'
+            else:
+                dict_list[dup_idx]['type'] = 'duplicated'
+        dict_list[dup_list[0]]['stop'] = dict_list[dup_list[-1]]['stop']
+        if dict_list[idx]['type'] == 'lead_sentence':
+            script_not_dup_list.append(dict_list[idx])
+                
+    #avoid subtitle overlapping ?   Timeline overlapping not found currently
+    #cut by max length---->  eng seperated problem   {eng_idx}
+    #ENG counts, zh counts, space counts
+
+    new_idx = 0
+    splitted_dict = []
+    for dic in script_not_dup_list:
+        dic_idx = 0
+        accumulated_duration = 0
+        duration = dic['stop']-dic['start']
+        for sub_dic in split_sentence(dic['content'],13):
+            new_dic = {}
+            new_dic['index'] = new_idx
+            if 'image_idx' in dic:
+                new_dic['image_obj'] = {'start':dic['start'],'idx':dic['image_idx']}
+            new_idx+=1
+            ind_duration = duration * sub_dic['time_ratio']
+            new_dic['start'] = dic['start'] + accumulated_duration
+            accumulated_duration += ind_duration
+            new_dic['content'] = sub_dic['content']
+            new_dic['duration'] = ind_duration*0.7
+            splitted_dict.append(new_dic)
+    
+    return splitted_dict
+
+
+
+def adjustSub_by_text_similarity(gts_in,gens_raw):
+    #call by value only
+    gts = gts_in[:]
+    text_parser = parser()
+    for i in range(len(gts)):
+        rep_ls = text_parser.replace_list(gts[i])
+        for reptxt in rep_ls:
+            gts[i] = gts[i].replace(reptxt,'')
+    print(gts)
+    gens = []
+    for idx in range(int((len(gens_raw)+1)/4)):
+        gens.append(gens_raw[idx*4+2])
+    
+    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:])]
+    alls = gts + combine2 + combine3
+
+    adjusted = [None]*len(gens)
+    duplicated_list = []
+    for idx in range(len(gens)):
+        match_text = difflib.get_close_matches(gens[idx], alls, cutoff=0.1)
+        if match_text[0] in duplicated_list:
+            for mt in match_text:
+                if mt == adjusted[idx-1] or mt not in duplicated_list:
+                    adjusted[idx] = mt
+                    break
+        else:
+            adjusted[idx] = match_text[0]
+            duplicated_list.append(match_text[0])
+    combine2_tag = [''.join([i,j]) for i,j in zip(gts_in, gts_in[1:])]
+    combine3_tag = [''.join([i,j,k]) for i,j,k in zip(gts_in, gts_in[1:], gts_in[2:])]
+    alls_tag = gts_in + combine2_tag + combine3_tag
+    for idx in range(len(adjusted)):
+        match_text = difflib.get_close_matches(adjusted[idx], alls_tag, cutoff=0.1)
+        adjusted[idx] = match_text[0]
+    return adjusted
+
+def trim_punctuation(s):
+    pat_block = u'[^\u4e00-\u9fff0-9a-zA-Z]+';
+    pattern = u'([0-9]+{0}[0-9]+)|{0}'.format(pat_block)
+    res = re.sub(pattern, lambda x: x.group(1) if x.group(1) else u" " ,s)
+    return res
+
+def splitter(s):
+    for sent in re.findall(u'[^!?,。\!\?]+[!? 。\!\?]?', s, flags=re.U):
+        yield sent
+
+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)):
+        img_list[idx]=[]
+        senList = split_by_pun(text_content[idx])
+        for inner_idx in range(len(senList)):
+            sv_path = dir_subtitle + name_hash +'/'+str(idx)+ str(inner_idx) +'.png'
+            sub = senList[inner_idx]
+            txt2image(sub,sv_path)
+            img_list[idx]+=[{"count":len(sub),"path":sv_path}]
+    return img_list
+
+def generate_subtitle_image_ENG(name_hash,text_content):
+    img_list = [None]*len(text_content)
+    for idx in range(len(text_content)):
+        sv_path = dir_subtitle + name_hash +'/'+str(idx)+'.png'
+        sub = text_content[idx]
+        txt2image(sub, sv_path,lang='eng')
+        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_long(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")
+    bg_left.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/"+tmp_video_dir+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",split_by_pun(text_content[0]))
+    for subd in sub_dict:
+        print(subd)
+    
+    generate_subtitle_image_from_dict(name_hash, 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 = [None] * len(sub_dict)
+    sub_clip_list = [None] * len(sub_dict)
+    for sub_obj in sub_dict:
+        idx = int(sub_obj['index'])
+        sub_img_list[idx] = openshot.QtImageReader(dir_subtitle + name_hash + '/' + str(idx)+'.png')
+        sub_img_list[idx].Open()
+        sub_clip_list[idx] = video_photo_clip(vid=sub_img_list[idx], 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_img_list[idx].Close()
+
+
+    tp = parser()
+    img_dict_ls = tp.image_clip_info(sub_dict)
+    #if 'image_idx' in dic:
+    #            new_dic['image_obj'] = {'start':dic['start'],'idx':dic['image_idx']}
+    img_clip_list = [None]*len(listdir(dir_photo+name_hash))
+    img_list = [None]*len(img_clip_list)
+    #for p in listdir(dir_photo+name_hash):
+    #    p_idx = int(p)-1
+    #    img_list[p_idx] = openshot.FFmpegReader(dir_photo+name_hash+'/'+p)
+    #    img_list[p_idx].Open()
+    #    img_clip_list[p_idx] = video_photo_clip(vid=img_list[p_idx],layer=3
+    #            ,scale_x=0.81,scale_y=0.68,location_y=-0.03,position=img_dict_ls[int(p)+1]['start'],end=img_dict_ls[int(p)+1]['duration'],audio=False)
+    #    t.AddClip(img_clip_list[p_idx])
+    #    img_list[p_idx].Close()
+    img_file_ls = listdir(dir_photo+name_hash)
+    print(img_file_ls)
+    print(img_dict_ls)
+    for img_idx in range(len(img_file_ls)):
+        img_list[img_idx] = openshot.FFmpegReader(dir_photo+name_hash+'/'+img_file_ls[img_idx])
+        img_list[img_idx].Open()
+        img_clip_list[img_idx] = video_photo_clip(vid=img_list[img_idx],layer=3
+                ,scale_x=0.81,scale_y=0.68,location_y=-0.03,position=img_dict_ls[img_idx]['start'],end=img_dict_ls[img_idx]['duration'],audio=False)
+        t.AddClip(img_clip_list[img_idx])
+        img_list[img_idx].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=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()
+    os.remove(tmp_video_dir+name_hash+"raw.mp4")
+    os.remove(tmp_video_dir+name_hash+"script.txt")
+    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')
+    print(multiLang)
+    file_prepare(name, name_hash, text_content,image_urls,multiLang)
+    sub_list=generate_subtitle_image(name_hash,text_content)
+    
+    for fname in range(len(text_content)):
+        call_anchor(name_hash+"/"+str(fname),avatar)
+        print('step finish')
+    print('called............................................')
+
+    ck=cKey(0,254,0,270)
+    ck_anchor=cKey(0,255,1,320)
+    duration = 0
+    #average layer level is 3
+    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
+    LOGO_OP_clip = video_photo_clip(vid=LOGO_OP,layer=4,position=0,end=LOGO_OP.info.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 += LOGO_OP.info.duration
+    head_duration = LOGO_OP.info.duration
+    bg_head.Close()
+    LOGO_OP.Close()
+
+    
+    clip_duration=0
+    photo_clip_list = [None]*len(text_content)
+    img_list = [None]*len(text_content)
+    anchor_clip_list = [None] * len(text_content)
+    anchor_list = [None] * len(text_content)
+    audio_clip_list = [None] * len(text_content)
+    audio_list = [None] * len(text_content)
+    sub_clip_list = [None] * len(text_content)
+    sub_img_list = [None] * len(text_content)
+    
+    idx = 0
+    for p in listdir(dir_photo+name_hash):
+        
+        anchor_list[idx] = openshot.FFmpegReader(dir_anchor+name_hash+"/"+str(idx)+".mp4")
+        clip_duration = anchor_list[idx].info.duration
+        anchor_list[idx].Open()
+        anchor_clip_list[idx] = video_photo_clip(vid=anchor_list[idx],layer=4,scale_x=0.65,scale_y=0.65,
+                location_x=0.35,location_y=0.25,position=main_timer, end=clip_duration,ck=ck_anchor,audio=False)
+        t.AddClip(anchor_clip_list[idx])
+
+        img_list[idx] = openshot.FFmpegReader(dir_photo+name_hash+'/'+p)
+        img_list[idx].Open()
+        photo_clip_list[idx] = video_photo_clip(vid=img_list[idx],layer=3
+                ,scale_x=0.81,scale_y=0.68,location_y=-0.03,position=main_timer,end=clip_duration,audio=False)
+        t.AddClip(photo_clip_list[idx])
+        img_list[idx].Close()
+
+        audio_list[idx] = openshot.FFmpegReader(dir_sound+name_hash+"/"+str(idx)+".mp3")
+        audio_list[idx].Open()
+        audio_clip_list[idx] = openshot.Clip(audio_list[idx])
+        audio_clip_list[idx].Position(main_timer)
+        audio_clip_list[idx].End(clip_duration)
+        t.AddClip(audio_clip_list[idx])
+
+        img_list[idx].Close()
+        anchor_list[idx].Close()
+        audio_list[idx].Close()
+            
+        sub_img_list[idx] = [None] * len(sub_list[idx])
+        sub_clip_list[idx] = [None] * len(sub_list[idx])
+        sub_timer = 0
+        for sub_idx in range(len(sub_list[idx])):
+            sub_img_list[idx][sub_idx] = openshot.QtImageReader(sub_list[idx][sub_idx]['path'])
+            sub_img_list[idx][sub_idx].Open()
+            sub_duration = 0.205*sub_list[idx][sub_idx]['count']
+            sub_clip_list[idx][sub_idx] = video_photo_clip(vid=sub_img_list[idx][sub_idx], layer=6,location_x=0.069, location_y=0.89,position=main_timer+sub_timer,end=sub_duration)
+            t.AddClip(sub_clip_list[idx][sub_idx])
+            sub_img_list[idx][sub_idx].Close()
+            sub_timer += sub_duration
+            print(sub_list[idx][sub_idx]['path'])
+        main_timer += clip_duration
+        idx+=1
+    
+    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+2
+                    ,location_x=0.005,location_y=-0.031
+                    ,scale_x=0.8,scale_y=0.6825)
+    t.AddClip(LOGO_ED_clip)
+    ED_duration = LOGO_ED.info.duration
+    LOGO_ED.Close()
+    
+
+    bg = openshot.FFmpegReader(dir_video+"complete_double_aispokesgirl.mp4")
+    bg.Open()
+    bg_times = math.floor(main_timer+ED_duration/bg.info.duration)
+    left_time = (main_timer+ED_duration) % 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")
+    bg_left.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)
+
+    ####start building
+    w = openshot.FFmpegWriter(tmp_video_dir+name_hash+".mp4")
+    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)
+    w.Open()
+    
+    #may change duration into t.info.duration
+    frames = int(t.info.fps)*int(head_duration+main_timer+ED_duration)
+    for n in range(frames):
+        f=t.GetFrame(n)
+        w.WriteFrame(f)
+        
+    #notify_group(name+"的影片已經產生完成囉! www.choozmo.com:8168/"+video_sub_folder+name_hash+".mp4")
+    t.Close()
+    w.Close()
+    print("video at : www.choozmo.com:8168/"+video_sub_folder+name_hash+".mp4")
+
+
+def anchor_video_eng(name_hash,name,text_content, image_urls,sub_titles,avatar):
+    file_prepare(name, name_hash, text_content,image_urls,'eng')
+    sub_list=generate_subtitle_image_ENG(name_hash,sub_titles)
+    
+    for fname in range(len(text_content)):
+        call_anchor(name_hash+"/"+str(fname),avatar)
+        print('step finish')
+    print('called............................................')
+
+    ck=cKey(0,254,0,270)
+    ck_anchor=cKey(0,255,1,320)
+    duration = 0
+    #average layer level is 3
+    t = openshot.Timeline(1280, 720, openshot.Fraction(30000, 1000), 44100, 2, openshot.LAYOUT_STEREO)
+    t.Open()
+
+    main_timer = 0
+    #add logo
+    LOGO_OP = openshot.FFmpegReader(dir_video+"LOGO_OP_4.mp4")
+    LOGO_OP.Open()         # Open the reader
+    LOGO_OP_clip = video_photo_clip(vid=LOGO_OP,layer=4,position=0,end=LOGO_OP.info.duration
+                    ,location_y=-0.03,scale_x=0.8,scale_y=0.704)
+    t.AddClip(LOGO_OP_clip)
+    #add background video  (head is different)
+    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 += LOGO_OP.info.duration
+    head_duration = LOGO_OP.info.duration
+    bg_head.Close()
+    LOGO_OP.Close()
+
+    #prepare empty list 
+    clip_duration=0
+    photo_clip_list = [None]*len(text_content)
+    img_list = [None]*len(text_content)
+    anchor_clip_list = [None] * len(text_content)
+    anchor_list = [None] * len(text_content)
+    audio_clip_list = [None] * len(text_content)
+    audio_list = [None] * len(text_content)
+    sub_clip_list = [None] * len(text_content)
+    #openshot image holder
+    sub_img_list = [None] * len(text_content)
+    
+    idx = 0
+    for p in listdir(dir_photo+name_hash):
+        
+        anchor_list[idx] = openshot.FFmpegReader(dir_anchor+name_hash+"/"+str(idx)+".mp4")
+        clip_duration = anchor_list[idx].info.duration
+        anchor_list[idx].Open()
+        anchor_clip_list[idx] = video_photo_clip(vid=anchor_list[idx],layer=4,scale_x=0.65,scale_y=0.65,
+                location_x=0.35,location_y=0.25,position=main_timer, end=clip_duration,ck=ck_anchor,audio=False)
+        t.AddClip(anchor_clip_list[idx])
+        #insert image 
+        img_list[idx] = openshot.FFmpegReader(dir_photo+name_hash+'/'+p)
+        img_list[idx].Open()
+        photo_clip_list[idx] = video_photo_clip(vid=img_list[idx],layer=3
+                ,scale_x=0.81,scale_y=0.68,location_y=-0.03,position=main_timer,end=clip_duration,audio=False)
+        t.AddClip(photo_clip_list[idx])
+        img_list[idx].Close()
+        #insert audio (speech)
+        audio_list[idx] = openshot.FFmpegReader(dir_sound+name_hash+"/"+str(idx)+".mp3")
+        audio_list[idx].Open()
+        audio_clip_list[idx] = openshot.Clip(audio_list[idx])
+        audio_clip_list[idx].Position(main_timer)
+        audio_clip_list[idx].End(clip_duration)
+        t.AddClip(audio_clip_list[idx])
+        #insert subtitle
+        sub_img_list[idx] = openshot.QtImageReader(sub_list[idx])
+        sub_img_list[idx].Open()
+        sub_clip_list[idx] = video_photo_clip(vid=sub_img_list[idx], layer=6,location_x=0.069, location_y=0.89,position=main_timer,end=clip_duration)
+        t.AddClip(sub_clip_list[idx])
+
+        img_list[idx].Close()
+        anchor_list[idx].Close()
+        audio_list[idx].Close()
+        sub_img_list[idx].Close()
+            
+        main_timer += clip_duration
+        idx+=1
+    
+    LOGO_ED = openshot.FFmpegReader(dir_video+"ED_ENG.mp4")
+    LOGO_ED.Open()
+    LOGO_ED_clip = video_photo_clip(vid=LOGO_ED,layer=4,position=main_timer,end=LOGO_ED.info.duration+2
+                    ,location_x=0.005,location_y=-0.031
+                    ,scale_x=0.8,scale_y=0.6825)
+    t.AddClip(LOGO_ED_clip)
+    ED_duration = LOGO_ED.info.duration
+    LOGO_ED.Close()
+    
+
+    bg = openshot.FFmpegReader(dir_video+"complete_double_aispokesgirl.mp4")
+    bg.Open()
+    bg_times = math.floor(main_timer+ED_duration/bg.info.duration)
+    left_time = (main_timer+ED_duration) % 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")
+    bg_left.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)
+
+    ####start building
+    w = openshot.FFmpegWriter(tmp_video_dir+name_hash+".mp4")
+    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)
+    w.Open()
+    
+    #may change duration into t.info.duration
+    frames = int(t.info.fps)*int(head_duration+main_timer+ED_duration)
+    for n in range(frames):
+        f=t.GetFrame(n)
+        w.WriteFrame(f)
+        
+    #notify_group(name+"(ENG)的影片已經產生完成囉! www.choozmo.com:8168/"+video_sub_folder+name_hash+".mp4")
+    t.Close()
+    w.Close()
+    print("video at : www.choozmo.com:8168/"+video_sub_folder+name_hash+".mp4")
+    #line notifs
+
+import pyttsx3
+def make_speech(text):
+    engine = pyttsx3.init()
+    #voices = engine.getProperty('voices')
+    engine.setProperty('voice', 'Mandarin')
+    engine.save_to_file(text, '/app/speech.mp3')
+    engine.runAndWait()
+
+    
+class video_service(rpyc.Service):
+    def exposed_call_video(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_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_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)
+    def exposed_make_speech(self,text):
+        make_speech(text)
+
+
+from rpyc.utils.server import ThreadedServer
+t = ThreadedServer(video_service, port=8858)
+print('service started')
+t.start()

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)

+ 41 - 0
OpenshotService/test.py

@@ -0,0 +1,41 @@
+
+def image_clip_info(dict_in):
+        #if 'image_idx' in dic:
+    #            new_dic['image_obj'] = {'start':dic['start'],'idx':dic['image_idx']}
+    stopPoint = 0 # sec
+    time_info = []
+    img_idx = 1 #start from 1
+    added_idx = []
+    for dic in dict_in:
+        if 'image_obj' in dic :
+            if dic['image_obj']['idx'] not in added_idx:
+                added_idx.append(dic['image_obj']['idx'])
+                time_info.append({'index':img_idx,'start':dic['start']})
+                img_idx += 1
+        stopPoint = dic['start']+dic['duration']
+        
+    for idx in range(len(time_info)-1):
+        time_info[idx]['duration'] = time_info[idx+1]['start']-time_info[idx]['start']
+    time_info[-1]['duration'] = stopPoint
+
+        #index start duration
+    return time_info        
+k=[{'index': 0, 'image_obj': {'start': 4.608, 'idx': 1}, 'start': 4.608, 'content': '露營車可分為拖曳式及自走', 'duration': 2.1504000000000003},
+{'index': 1, 'image_obj': {'start': 4.608, 'idx': 1}, 'start': 7.68, 'content': '式', 'duration': 0.1791999999999999},
+{'index': 2, 'start': 8.192, 'content': '拖曳式即是俗稱「 露營拖', 'duration': 1.5487999999999993},
+{'index': 3, 'start': 10.404571428571428, 'content': '車」 ', 'duration': 0.42239999999999983},
+{'index': 4, 'start': 11.264, 'content': '前方需仰賴母車牽引才能移', 'duration': 2.1504000000000008},
+{'index': 5, 'start': 14.336, 'content': '動', 'duration': 0.17919999999999991},
+{'index': 6, 'image_obj': {'start': 14.848, 'idx': 2}, 'start': 14.848, 'content': '現今市場已有很多小客車都', 'duration': 1.7644307692307686},
+{'index': 7, 'image_obj': {'start': 14.848, 'idx': 2}, 'start': 17.368615384615385, 'content': '附有拖曳功能。 ', 'duration': 1.10276923076923},
+{'index': 8, 'start': 19.456, 'content': '拖車車廂內設備因車主需求', 'duration': 1.9967999999999995},
+{'index': 9, 'start': 22.308571428571426, 'content': '而異', 'duration': 0.33280000000000004},
+{'index': 10, 'image_obj': {'start': 23.04, 'idx': 3}, 'start': 23.04, 'content': '一 般而言大多會有床鋪、 ', 'duration': 1.7740800000000003},
+{'index': 11, 'image_obj': {'start': 23.04, 'idx': 3}, 'start': 25.5744, 'content': '小桌、 冰箱、 迷你廚房', 'duration': 1.6128},
+{'index': 12, 'image_obj': {'start': 23.04, 'idx': 3}, 'start': 27.8784, 'content': '或獨立衛浴等便於露營生活', 'duration': 1.93536},
+{'index': 13, 'image_obj': {'start': 23.04, 'idx': 3}, 'start': 30.6432, 'content': '的各項配備', 'duration': 1.1289600000000004},
+{'index': 14, 'start': 32.512, 'content': '具備動力系統的自走式露營', 'duration': 3.8358486486486485},
+{'index': 15, 'start': 37.99178378378379, 'content': '車價格較高。 ', 'duration': 2.0777513513513512},
+{'index': 16, 'start': 33.28, 'content': '規格有大有小', 'duration': 1.0752000000000008},
+{'index': 17, 'start': 35.072, 'content': '就歐美車款來說', 'duration': 1.254399999999996}]
+ls = image_clip_info(k)

BIN
OpenshotService/util/__pycache__/parser.cpython-39.pyc


+ 57 - 0
OpenshotService/util/parser.py

@@ -0,0 +1,57 @@
+import re
+
+class parser:
+
+    def __init__(self):
+        print("Address of self = ",id(self))
+
+    def check_image_count(self,image_list, text):
+        pair_obj = findTag(text)
+        if pair_obj['code'] == 1:
+            if len(image_list)!=len(pair_obj['pair']):
+                return {'msg':'圖片與標籤數量不一致!','code':-1}
+            else:
+                return {code:1}
+        else:
+            return pair_obj
+            
+
+    def findTag(self,text):
+        left_tag = [m.start() for m in re.finditer('{', text)]
+        right_tag = [m.start() for m in re.finditer('}', text)]
+        if len(left_tag)!=len(right_tag):
+            return {'msg':'圖片標籤錯誤,左右數量不符','code':-1}
+        pair = []
+        for idx in range(len(left_tag)):
+            pair.append({'left':left_tag[idx],'right':right_tag[idx]})
+        obj = {'code':1,'pair':pair}
+        return obj
+
+    def replace_list(self,text):
+        rep_ls = []
+        pair_obj = self.findTag(text)
+        for p in pair_obj['pair']:
+            rep_ls.append(text[p['left']:p['right']+1])
+        return rep_ls
+    
+    def image_clip_info(self,dict_in):
+        #if 'image_idx' in dic:
+    #            new_dic['image_obj'] = {'start':dic['start'],'idx':dic['image_idx']}
+        stopPoint = 0 # sec
+        time_info = []
+        img_idx = 1 #start from 1
+        added_idx = []
+        for dic in dict_in:
+            if 'image_obj' in dic :
+                if dic['image_obj']['idx'] not in added_idx:
+                    added_idx.append(dic['image_obj']['idx'])
+                    time_info.append({'index':img_idx,'start':dic['start']})
+                    img_idx += 1
+            if dic['start']+dic['duration'] > stopPoint:
+                stopPoint = dic['start']+dic['duration']
+        
+        for idx in range(len(time_info)-1):
+            time_info[idx]['duration'] = time_info[idx+1]['start']-time_info[idx]['start']
+        time_info[-1]['duration'] = stopPoint - time_info[-1]['start']
+        #index start duration
+        return time_info               

BIN
api/.DS_Store


BIN
api/__pycache__/gSlide.cpython-39.pyc


BIN
api/__pycache__/mailer.cpython-39.pyc


BIN
api/__pycache__/main.cpython-39.pyc


BIN
api/__pycache__/models.cpython-39.pyc


+ 79 - 0
api/gSlide.py

@@ -0,0 +1,79 @@
+
+
+from __future__ import print_function
+import os.path
+from googleapiclient.discovery import build
+from google_auth_oauthlib.flow import InstalledAppFlow
+from google.auth.transport.requests import Request
+from google.oauth2.credentials import Credentials
+from google.oauth2 import service_account
+import argparse
+import requests
+import pprint
+import json
+import calendar
+import time
+import os
+import shutil
+from distutils.util import strtobool
+
+SCOPES = ['https://www.googleapis.com/auth/presentations.readonly',
+          'https://www.googleapis.com/auth/drive.metadata.readonly']
+
+dir_sound = 'mp3_track/'
+dir_photo = 'photo/'
+dir_text = 'text_file/'
+dir_video = 'video_material/'
+dir_title = 'title/'
+dir_subtitle = 'subtitle/'
+dir_anchor = 'anchor_raw/'
+tmp_video_dir = 'tmp_video/'
+video_sub_folder = 'ai_anchor_video/'
+
+def parse_url(url):
+    #https://docs.google.com/presentation/d/17jJ3OZWh8WorFcolB_LiTa7xQ3R-xrmFlqJ_EyCj06M/edit#slide=id.p
+    return url.split('/')[5]
+
+def parse_slide_url(slide_url,eng):
+    PRESENTATION_ID = parse_url(slide_url)
+    credentials = service_account.Credentials.from_service_account_file('spread2.json')
+    scoped_credentials = credentials.with_scopes(SCOPES)
+    creds = credentials
+
+    notes_list=[]
+    sub_title_list=[]
+    img_list=[]
+
+    service = build('slides', 'v1', credentials=creds)
+        # Call the Slides API
+    presentation = service.presentations().get(
+        presentationId=PRESENTATION_ID).execute()
+    slides = presentation.get('slides')
+        
+    for i, slide in enumerate(slides):
+        # Check if the notes exists
+        if 'text' in slide['slideProperties']['notesPage']['pageElements'][1]['shape'].keys():
+            notes = slide['slideProperties']['notesPage']['pageElements'][1]['shape']['text']['textElements'][1]['textRun']['content']
+            if '[sub_title]' in notes:
+                sub_title = notes.split('[sub_title]')[1].strip()
+                print('Sub_title:',end='')
+                pprint.pprint(sub_title)
+                sub_title_list.append(sub_title)
+                notes = notes.split('[sub_title]')[0].strip()
+            pprint.pprint(notes)
+            notes_list.append(notes)  
+        else:
+            notes_list.append("")
+                
+        # Convert the content of the presentation to png
+        thumbnail = service.presentations().pages().getThumbnail(presentationId=PRESENTATION_ID, pageObjectId=slide['objectId']).execute()
+        pprint.pprint(thumbnail)
+        img_list.append(thumbnail['contentUrl'])
+            
+    # data
+    slide_content = { "name": presentation['title'], "text_content": notes_list, "image_urls": img_list, "avatar": "7", "client_id": calendar.timegm(time.gmtime()) }
+    if eng:
+        slide_content['sub_titles'] = sub_title_list
+    print(slide_content)
+    return slide_content['name'],slide_content['text_content'],slide_content['image_urls']
+        

+ 57 - 0
api/mailer.py

@@ -0,0 +1,57 @@
+import smtplib
+import traceback
+import os
+from email.mime.text import MIMEText
+from email.mime.image import MIMEImage
+from email.mime.multipart import MIMEMultipart
+from email.message import EmailMessage
+import codecs
+
+gmail_user = 'ming@choozmo.com'
+gmail_password='lzpxinencaawsjus'
+
+sent_from = gmail_user
+#to = ['jared@choozmo.com','nina.huang@choozmo.com','ana@choozmo.com','ming@choozmo.com','mike@choozmo.com','andy@choozmo.com','hana@choozmo.com','stacy@choozmo.com','wen@choozmo.com','yukyo@choozmo.com','fxp87257@gmail.com','noodlesloves@gmail.com']
+
+def send_left_not_enough(msg_in,email):
+    to = [email]
+    msg = MIMEMultipart()
+    msg['Subject'] = 'AI Spokesgirl 服務餘額不足 '
+    
+    msgAlternative = MIMEMultipart('alternative')
+    msg.attach(msgAlternative)
+
+    text = MIMEText(msg_in,'html','utf-8')
+
+    msgAlternative.attach(text)
+    try:
+        server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
+        server.ehlo()
+        server.login(gmail_user, gmail_password)
+        server.sendmail(sent_from, to, msg.as_string())
+        server.close()
+        print ('Email sent!')
+    except:
+        traceback.print_exc()
+        print ('Something went wrong...')
+
+def register_verify(msg_in,email):
+    to = [email]
+    msg = MIMEMultipart()
+    msg['Subject'] = '驗證您的信箱'
+    
+    msgAlternative = MIMEMultipart('alternative')
+    msg.attach(msgAlternative)
+
+    text = MIMEText(msg_in,'html','utf-8')
+
+    msgAlternative.attach(text)
+    try:
+        server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
+        server.ehlo()
+        server.login(gmail_user, gmail_password)
+        server.sendmail(sent_from, to, msg.as_string())
+        server.close()
+    except:
+        traceback.print_exc()
+

+ 709 - 0
api/main.py

@@ -0,0 +1,709 @@
+from fastapi import FastAPI,Cookie, Depends, Query, status,File, UploadFile,Request,Response,HTTPException
+from fastapi.templating import Jinja2Templates
+from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
+from pydantic import BaseModel
+from typing import List, Optional
+from os.path import isfile, isdir, join
+import threading
+import zhtts
+import os 
+import urllib
+import requests
+from bs4 import BeautifulSoup
+from PIL import Image,ImageDraw,ImageFont
+import pyttsx3
+import rpyc
+import random
+import time
+import math
+import hashlib
+import re
+import asyncio
+import urllib.request
+from fastapi.responses import FileResponse
+from fastapi.middleware.cors import CORSMiddleware
+import dataset
+from datetime import datetime, timedelta
+from util.swap_face import swap_face
+from fastapi.staticfiles import StaticFiles
+import shutil
+import io
+from first import first
+from passlib.context import CryptContext
+from jose import JWTError, jwt
+from fastapi_jwt_auth import AuthJWT
+from fastapi_jwt_auth.exceptions import AuthJWTException
+from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
+import models
+import pymysql
+from first import first
+import mailer
+from moviepy.editor import VideoFileClip
+import traceback
+import logging
+import gSlide
+import aiofiles
+import json
+pymysql.install_as_MySQLdb()
+
+app = FastAPI()
+
+
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+SECRET_KEY = "df2f77bd544240801a048bd4293afd8eeb7fff3cb7050e42c791db4b83ebadcd"
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = 3000
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+app.mount("/static", StaticFiles(directory="static"), name="static")
+app.mount("/static/img", StaticFiles(directory="static/img"), name="static/img")
+app.mount("/templates", StaticFiles(directory="templates"), name="templates")
+
+templates = Jinja2Templates(directory="templates")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+tmp_video_dir = '../OpenshotService/tmp_video/'
+tmp_avatar_dir = '../../face_swap/tmp_avatar/'  #change source face path here
+
+video_sub_folder = 'ai_anchor_video/'
+avatar_sub_folder = 'swap_save/'
+tmp_img_sub_folder = 'tmp_img/'
+img_upload_folder = '/var/www/html/'+tmp_img_sub_folder
+video_dest = '/var/www/html/'+video_sub_folder
+avatar_dest = '/var/www/html/'+avatar_sub_folder
+
+
+# @app.get("/index2")
+# async def index2():
+#     return FileResponse('static/index2.html')
+
+@app.get("/index_eng")
+async def index2():
+    return FileResponse('static/index_eng.html')
+
+# home page
+@app.get("/index", response_class=HTMLResponse)
+async def get_home_page(request: Request, response: Response):
+    return templates.TemplateResponse("index.html", {"request": request, "response": response})
+@app.get("/", response_class=HTMLResponse)
+async def get_home_page(request: Request, response: Response):
+    return templates.TemplateResponse("index.html", {"request": request, "response": response})
+
+@app.get("/make_video", response_class=HTMLResponse)
+async def get_home_page(request: Request, response: Response, Authorize: AuthJWT = Depends()):
+    try:
+        Authorize.jwt_required()
+    except Exception as e:
+        print(e)
+        return '請先登入帳號'
+    current_user = Authorize.get_jwt_subject()
+    return templates.TemplateResponse("make_video.html", {"request": request, "response": response})
+
+@app.get("/make_video_long", response_class=HTMLResponse)
+async def get_home_page(request: Request, response: Response, Authorize: AuthJWT = Depends()):
+    try:
+        Authorize.jwt_required()
+    except Exception as e:
+        print(e)
+        return '請先登入帳號'
+    current_user = Authorize.get_jwt_subject()
+    return templates.TemplateResponse("make_video_long.html", {"request": request, "response": response})
+
+@app.get("/make_video_slide", response_class=HTMLResponse)
+async def make_video_slide(request: Request, response: Response, Authorize: AuthJWT = Depends()):
+    try:
+        Authorize.jwt_required()
+    except Exception as e:
+        print(e)
+        return '請先登入帳號'
+    current_user = Authorize.get_jwt_subject()
+    return templates.TemplateResponse("make_video_slide.html", {"request": request, "response": response})
+
+@app.get('/user_profile', response_class=HTMLResponse)
+def protected(request: Request, Authorize: AuthJWT = Depends()):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    Authorize.jwt_required()
+    current_user = Authorize.get_jwt_subject()
+    user_obj = first(db.query('SELECT * FROM users where username ="'+str(current_user)+'"'))
+    video_num = str(first(db.query('SELECT COUNT(*) FROM history_input WHERE user_id ='+str(user_obj['id'])))['COUNT(*)'])
+    total_sec = str(first(db.query('SELECT SUM(duration) FROM history_input where user_id='+str(user_obj['id'])))['SUM(duration)'])
+    left_sec = user_obj['left_time']
+
+    video_info_list = []
+    statement = 'SELECT * FROM history_input WHERE user_id='+str(user_obj['id'])
+    for row in db.query(statement):
+        video_info_list.append({'id':row['id'],'title':row['name'],'duration':row['duration'],'url':row['url']})
+    dic_return = {'user_info':{'userName':current_user,'email':user_obj['email'],'video_num':video_num,'total_sec':total_sec,'left_sec':user_obj['left_time']},'video_info':video_info_list}
+    str_return = json.dumps(dic_return)
+    return str_return
+
+# login & register page
+@app.get("/login", response_class=HTMLResponse)
+async def get_login_and_register_page(request: Request):
+    return templates.TemplateResponse("login.html", {"request": request})
+
+@app.post("/login")
+async def login_for_access_token(request: Request, form_data: OAuth2PasswordRequestForm = Depends(), Authorize: AuthJWT = Depends()):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    user = authenticate_user(form_data.username, form_data.password)
+    if not user:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Incorrect username or password",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+    access_token = create_access_token(
+        data={"sub": user.username}, expires_delta=access_token_expires
+    )
+    table = db['users']
+    user.token = access_token
+    table.update(dict(user), ['username'])
+    access_token = Authorize.create_access_token(subject=user.username)
+    refresh_token = Authorize.create_refresh_token(subject=user.username)
+    Authorize.set_access_cookies(access_token)
+    Authorize.set_refresh_cookies(refresh_token)
+    #return templates.TemplateResponse("index.html", {"request": request, "msg": 'Login'})
+    return {"access_token": access_token, "token_type": "bearer"}
+
+
+@app.post("/token")
+async def access_token(form_data: OAuth2PasswordRequestForm = Depends(), Authorize: AuthJWT = Depends()):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    user = authenticate_user(form_data.username, form_data.password)
+    if not user:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Incorrect username or password",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+    access_token = create_access_token(
+        data={"sub": user.username}, expires_delta=access_token_expires
+    )
+    return {"access_token": access_token, "token_type": "bearer"}
+
+
+@app.post("/register")
+async def register(request: Request):
+    user = models.User(**await request.form())
+    user_obj = first(db.query('SELECT * FROM users where username ="'+str(user.username)+'"'))
+    if user_obj != None:
+        user_register(user)
+        return templates.TemplateResponse("login.html", {'request': request,"success": True}, status_code=status.HTTP_302_FOUND)
+    else:
+        return {'msg':user.username+'重複,請更改'}
+
+
+@app.get('/logout')
+def logout(request: Request, Authorize: AuthJWT = Depends()):
+    Authorize.jwt_required()
+    Authorize.unset_jwt_cookies()
+    return {"msg": "Successfully logout"}
+
+@app.get("/gen_avatar")
+async def avatar():
+    return FileResponse('static/gen_avatar.html')
+
+@app.post("/swapFace")
+async def swapFace(req:models.swap_req):
+    if 'http' not in req.imgurl:
+        req.imgurl= 'http://'+req.imgurl
+    try:
+        im = Image.open(requests.get(req.imgurl, stream=True).raw)
+        im= im.convert("RGB")
+    except:
+        return {'msg':"無法辨別圖片網址"+req.imgurl}
+    name_hash = str(time.time()).replace('.','')
+    
+    x = threading.Thread(target=gen_avatar, args=(name_hash,req.imgurl))
+    x.start()
+    return {'msg':'人物生成中,請稍候'}
+
+@app.post("/uploadfile/")
+async def create_upload_file(file: UploadFile = File(...)):
+    img_name = str(time.time()).replace('.','')
+    try:
+        if file.content_type=='video/mp4':
+            async with aiofiles.open(img_upload_folder+img_name+'.mp4', 'wb') as out_file:
+                content = await file.read()
+                await out_file.write(content) 
+            return {"msg": 'www.choozmo.com:8168/'+tmp_img_sub_folder+img_name+'.mp4'}
+        else:
+            contents = await file.read()
+            image = Image.open(io.BytesIO(contents))
+            image= image.convert("RGB")
+            image.save(img_upload_folder+img_name+'.jpg')
+            return {"msg": 'www.choozmo.com:8168/'+tmp_img_sub_folder+img_name+'.jpg'}
+    except Exception as e:
+        logging.error(traceback.format_exc())
+        return {'msg':'檔案無法使用'}
+
+@app.post("/make_anchor_video_gSlide")
+async def make_anchor_video_gSlide(req:models.gSlide_req,token: str = Depends(oauth2_scheme)):
+    name, text_content, image_urls = gSlide.parse_slide_url(req.slide_url,eng=False)
+    if len(image_urls) != len(text_content):
+        return {'msg':'副標題數量、圖片(影片)數量以及台詞數量必須一致'}
+    for idx in range(len(image_urls)):
+        if 'http' not in image_urls[idx]:
+            image_urls[idx] = 'http://'+image_urls[idx]
+    if req.multiLang==0:
+        for txt in text_content:
+            if re.search('[a-zA-Z]', txt) !=None:
+                print('語言錯誤')
+                return {'msg':'輸入字串不能包含英文字!'}
+    name_hash = str(time.time()).replace('.','')
+    for imgu in 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)
+    proto_req = models.request_normal()
+    proto_req.text_content = text_content
+    proto_req.name = name
+    proto_req.image_urls = image_urls
+    proto_req.avatar = req.avatar
+    proto_req.multiLang = req.multiLang
+    video_id = save_history(proto_req,name_hash,user_id)
+    x = threading.Thread(target=gen_video_queue, args=(name_hash,name, text_content, image_urls,int(req.avatar),req.multiLang,video_id,user_id))
+    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)):
+    left_tag = [m.start() for m in re.finditer('{', req.text_content[0])]
+    if len(req.image_urls) != len(left_tag):
+        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)
+    video_id = 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,video_id,user_id))
+    x.start()
+    return {"msg":"ok"} 
+
+@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):
+        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)
+    video_id = save_history(req,name_hash,user_id)
+    x = threading.Thread(target=gen_video_queue, args=(name_hash,req.name, req.text_content, req.image_urls,int(req.avatar),req.multiLang,video_id,user_id))
+    x.start()
+    return {'msg':'ok'}
+
+@app.post("/make_anchor_video_eng")
+async def make_anchor_video_eng(req:models.request_eng):
+    if len(req.image_urls) != len(req.sub_titles) or len(req.sub_titles) != 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]
+    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}
+
+    video_id = save_history(req,name_hash)
+    x = threading.Thread(target=gen_video_queue_eng, args=(name_hash,req.name, req.text_content, req.image_urls,req.sub_titles,int(req.avatar),video_id))
+    x.start()
+    return {"msg":"ok"} 
+
+@app.get("/history_input")
+async def history_input(request: Request, Authorize: AuthJWT = Depends()):
+    Authorize.jwt_required()
+    current_user = Authorize.get_jwt_subject()
+
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    user_id = first(db.query('SELECT * FROM users where username="' + current_user +'"'))['id']
+    statement = 'SELECT * FROM history_input WHERE user_id="'+str(user_id)+'" ORDER BY timestamp DESC LIMIT 50'
+
+    logs = []
+    for row in db.query(statement):
+        logs.append({'id':row['id'],'name':row['name'],'text_content':row['text_content'].split(','),'link':row['link'],'image_urls':row['image_urls'].split(',')})
+    return logs
+
+
+
+@AuthJWT.load_config
+def get_config():
+    return models.Settings()
+
+@app.exception_handler(AuthJWTException)
+def authjwt_exception_handler(request: Request, exc: AuthJWTException):
+    return JSONResponse(
+        status_code=exc.status_code,
+        content={"detail": exc.message}
+    )
+
+def get_user_id(token):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    credentials_exception = HTTPException(
+        status_code=status.HTTP_401_UNAUTHORIZED,
+        detail="Could not validate credentials",
+        headers={"WWW-Authenticate": "Bearer"},
+    )
+    try:
+        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+        username: str = payload.get("sub")
+        if username is None:
+            raise credentials_exception
+        token_data = models.TokenData(username=username)
+    except JWTError:
+        raise credentials_exception
+    user = get_user(username=token_data.username)
+    if user is None:
+        raise credentials_exception
+    user_id = first(db.query('SELECT * FROM users where username="' + user.username+'"'))['id']
+    return user_id
+
+def check_user_exists(username):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    if int(next(iter(db.query('SELECT COUNT(*) FROM AI_anchor.users WHERE username = "'+username+'"')))['COUNT(*)']) > 0:
+        return True
+    else:
+        return False
+
+def get_user(username: str):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    if not check_user_exists(username):  # if user don't exist
+        return False
+    user_dict = next(
+        iter(db.query('SELECT * FROM AI_anchor.users where username ="'+username+'"')))
+    user = models.User(**user_dict)
+    return user
+    
+def user_register(user):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    table = db['users']
+    user.password = get_password_hash(user.password)
+    table.insert(dict(user))
+
+def get_password_hash(password):
+    return pwd_context.hash(password)
+def verify_password(plain_password, hashed_password):
+    return pwd_context.verify(plain_password, hashed_password)
+def authenticate_user(username: str, password: str):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    if not check_user_exists(username):  # if user don't exist
+        return False
+    user_dict = next(iter(db.query('SELECT * FROM AI_anchor.users where username ="'+username+'"')))
+    user = models.User(**user_dict)
+    if not verify_password(password, user.password):
+        return False
+    return user
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
+    to_encode = data.copy()
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+    else:
+        expire = datetime.utcnow() + timedelta(minutes=15)
+    to_encode.update({"exp": expire})
+    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+    return encoded_jwt
+
+def save_history(req,name_hash,user_id):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    log_table = db['history_input']
+    txt_content_seperate_by_dot = ''
+    for txt in req.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 req.image_urls:
+        img_urls_seperate_by_dot += iurl+","
+    img_urls_seperate_by_dot = img_urls_seperate_by_dot[:-1]
+    time_stamp = datetime.fromtimestamp(time.time())
+    time_stamp = time_stamp.strftime("%Y-%m-%d %H:%M:%S")
+    pk = log_table.insert({'name':req.name,'text_content':txt_content_seperate_by_dot,'image_urls':img_urls_seperate_by_dot
+    ,'user_id':user_id,'link':'www.choozmo.com:8168/'+video_sub_folder+name_hash+'.mp4','timestamp':time_stamp})
+    return pk
+    
+def get_url_type(url):
+    req = urllib.request.Request(url, method='HEAD', headers={'User-Agent': 'Mozilla/5.0'})
+    r = urllib.request.urlopen(req)
+    contentType = r.getheader('Content-Type')
+    return contentType
+
+def notify_group(msg):
+    glist=['7vilzohcyQMPLfAMRloUawiTV4vtusZhxv8Czo7AJX8','WekCRfnAirSiSxALiD6gcm0B56EejsoK89zFbIaiZQD','1dbtJHbWVbrooXmQqc4r8OyRWDryjD4TMJ6DiDsdgsX','HOB1kVNgIb81tTB4Ort1BfhVp9GFo6NlToMQg88vEhh']
+    for gid in glist:
+        headers = {"Authorization": "Bearer " + gid,"Content-Type": "application/x-www-form-urlencoded"}
+        r = requests.post("https://notify-api.line.me/api/notify",headers=headers, params={"message": msg})
+
+
+def gen_video_long_queue(name_hash,name,text_content, image_urls,avatar,multiLang,video_id,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))
+            db.query('UPDATE history_input SET duration ='+str(vid_duration)+' WHERE id='+str(video_id)+';')
+            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_left_not_enough(msg, email)
+                #notify_line_user(msg, line_token)
+                notify_group(name+":帳號餘額不足,請至email查看詳細資訊")
+            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,video_id,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(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))
+            db.query('UPDATE history_input SET duration ='+str(vid_duration)+' WHERE id='+str(video_id)+';')
+            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_left_not_enough(msg, email)
+                notify_group(msg)
+                #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_eng(name_hash,name,text_content, image_urls,sub_titles,avatar,video_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]
+    subtitles_seperate_by_dot = ''
+    for sub in sub_titles:
+        subtitles_seperate_by_dot += sub+","
+    subtitles_seperate_by_dot = subtitles_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,'subtitles':subtitles_seperate_by_dot,'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')
+            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:
+            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_eng(top1['name_hash'],top1['name'],top1['text_content'].split(','), top1['image_urls'].split(','),top1['subtitles'].split(','),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')
+            notify_group(name+"(ENG)的影片已經產生完成囉! www.choozmo.com:8168/"+video_sub_folder+name_hash+".mp4")
+        except:
+            print('video generation error')
+            notify_group('影片錯誤')
+        db['video_queue'].delete(id=top1['id'])
+        db.query('UPDATE video_queue_status SET status = 0')
+
+def gen_avatar(name_hash, imgurl):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    db['avatar_queue'].insert({'name_hash':name_hash,'imgurl':imgurl})
+    while True:
+        statement = 'SELECT * FROM avatar_service_status'#only one row in this table, which is the id 1 one
+        status = -1
+        for row in db.query(statement):
+            status = row['status']
+        if status == 1:
+            print('leave process loop')
+            break
+
+        statement = 'SELECT * FROM avatar_queue'
+        works = []
+        for row in db.query(statement):
+            works.append({'id':row['id'],'name_hash':row['name_hash'],'imgurl':row['imgurl']})
+        if len(works)==0:
+            print('leave process loop')
+            break
+        try:
+            statement = 'UPDATE avatar_service_status SET status = 1 WHERE id=1;'
+            db.query(statement)
+            name_hash = works[0]['name_hash']
+            imgurl = works[0]['imgurl']
+            c = rpyc.connect("localhost", 8868)
+            c._config['sync_request_timeout'] = None
+            remote_svc = c.root
+            my_answer = remote_svc.call_avatar(name_hash,imgurl) # method call
+            shutil.copy(tmp_avatar_dir+name_hash+'.mp4',avatar_dest+name_hash+'.mp4')
+            os.remove(tmp_avatar_dir+name_hash+'.mp4')
+            
+        except:
+            print('gen error')
+            notify_group('無法辨識人臉')
+        db['avatar_queue'].delete(id=works[0]['id'])
+        statement = 'UPDATE avatar_service_status SET status = 0 WHERE id=1;'  #only one row in this table, which id 1 one
+        db.query(statement)
+def call_voice(text):
+    print(text)
+    print(len(text))
+    print(type(text))
+    c = rpyc.connect("localhost", 8858)
+    c._config['sync_request_timeout'] = None
+    remote_svc = c.root
+    my_answer = remote_svc.make_speech(text) # method call
+    src_path = '/home/ming/AI_Anchor/OpenshotService/speech.mp3'
+    shutil.copy(src_path,'/home/ming/speech.mp3')
+    os.remove(src_path)
+
+class text_in(BaseModel):
+    text: str
+
+@app.post("/make_voice")
+async def make_voice(in_text:text_in):
+    x = threading.Thread(target=call_voice, args=(in_text.text,))
+    x.start()
+    
+        
+
+    
+      
+        
+        
+        
+
+
+

+ 58 - 0
api/models.py

@@ -0,0 +1,58 @@
+from pydantic import BaseModel
+from typing import List, Optional
+SECRET_KEY = "df2f77bd544240801a048bd4293afd8eeb7fff3cb7050e42c791db4b83ebadcd"
+ALGORITHM = "HS256"
+class Token(BaseModel):
+    access_token: str
+    token_type: str
+
+class swap_req(BaseModel):
+    imgurl: str
+
+class request(BaseModel):
+    name: str
+    text_content: List[str]
+    image_urls: List[str]
+    avatar: str
+    client_id :str
+    multiLang :int
+
+class request_normal():
+    name: str
+    text_content: List[str]
+    image_urls: List[str]
+    avatar: str
+    client_id :str
+    multiLang :int
+
+class gSlide_req(BaseModel):
+    slide_url: str
+    avatar: str
+    client_id :str
+    multiLang :int
+
+class request_eng(BaseModel):
+    name: str
+    text_content: List[str]
+    image_urls: List[str]
+    sub_titles: List[str]
+    avatar: str
+    client_id :str
+# AuthJWT setting
+class Settings(BaseModel):
+    authjwt_secret_key: str = SECRET_KEY
+    authjwt_token_location: set = {"cookies"}
+    authjwt_cookie_secure: bool = False
+    authjwt_cookie_csrf_protect: bool = True
+
+class User(BaseModel):
+    username: str
+    email: str
+    password: str
+    token: Optional[str] = None
+    
+class TokenData(BaseModel):
+    username: Optional[str] = None
+
+class phone(BaseModel):
+    price: int

+ 12 - 0
api/spread2.json

@@ -0,0 +1,12 @@
+{
+  "type": "service_account",
+  "project_id": "dstest-1-292707",
+  "private_key_id": "41b3cec48b4af2e91b89cf6c1644b2fbdf603a72",
+  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC47O9j5gNkeWsY\n7XsBwl08z/pLNKrKPq8zYoI2tlnctHAOowjSkwXRAuERUeY+O4cRn8HNRqtnZ5M/\nt3kLEtmQpQClk7A8pv21bo4lT8nILZLWU1ovo3bzFNaBN7I/1PNUzSm8G5w4aUSy\noO2/beQLoz0gs1e1WlLAQVkS4NvUFagP/nY3sAgW/1mZSeNsHk8x8VLc1t6rIlSl\n9wPQ52KBCTUUu6gveQ5nzpOZ3eNxr+ftllF378tt/tzCqFdI0DkYYv7jxSAfXa+y\nmlHoSE8mhd+M4hvrf4E5jjvdfyNmALiBpyEW2YAMe1qK0Ay3aUodZxSIKCU8FYo2\njIVCBxTtAgMBAAECggEAVUdafECx1s9RbrzxaVXHJoiona7rhfnTVMh8URvVo/yH\n4pAXvPf1CjagMRsKKS/NcXixaGdLGxP+KTeEd/FY6KXW/wR1FPtTa6xQm+9IF+rA\nWNs2b1qcO6wj4ZIcPuiG0FgOg4NeDvuopRGmog1cyWsdgOuDqou9NpDMMXMFnS8Y\nivS3P1pKXSbg7XKQ4mCWfQk5Oq8Uf3OrOOQncFvUazWQDnhI7GZ9yLw+pqCeziWv\nUeGql2EKtjEsOj9zaN4AeGoSyZDcgVo3X4XRR+sq/Jqd2D8m0aQmlMEcEIVLPmtH\nkugDJsS+Yuk7YQFX4pMbKOCYlO2XCX1STPAn+ytxFwKBgQDlkMG8dvCAn3qoTjlh\n4qj8AHlcyZghcR9Ru1qnSZbw7wVBiTVD8L6mEiLmEGumkNHnwnE/s0PuU10POXeu\nrD4AYtLnJ5f42ycsbBw+ncc1qCwqBwdx4Vo0QXNBs4S+zmf6M8fuzT5wWnaHP5Tg\nbdKByPlpIoGwy/7XseRi3qkQLwKBgQDOOEK3fZ+PQ7PBCGw8DcfWXL4dsUoa5KuC\ncZbkE+sOyGAGBwN+UvxxLQqkSLDtuki6t9+9DvtkfY5N79JN5fGId+BBg8HdhCI+\nxKElGj52f1lcukhMJ3zhLsbUFWz0UFJDudG+qaUuqHRjVCLRJTzlTrvqjHtYLwyO\nQPTi5QnpowKBgQDlURbWZpGUSrrCCXH0v/BB209gSti2/0Nj552E4lPvVTSQ5Lja\np1AqoI9P9jMy7hNgSbHLCg3fslKRdLyDNfexdwZqdfivVGvrSgtk2UM37EhBq0fa\nkFwFOyQhC2ydFZ50JumfOFMY9KTWMcNL9SiFEPdj+F5I914YpNEZmoaTbQKBgF+8\nXLTEvEONYbD20RCcMS8CRTyRpt6PVFQtmahu2sw1F+cUcHm/2vRLvcoA+SqUNdmB\nLXyerPS9GUhzUsXZP2VkiZbArUrCYgeTz1/jLCZk/r5+uLuqBV6hEas5+yf89gP9\nCzOhnE7p44aNc9B2oiuufqzn5QdOaFzOKSIAxLZTAoGBAIicGUmg/FPXj9TC7/8E\nRX7TBFEmJOt+cQNCQZ2KLJD4Io1v7tISjyv2dkYxQZ2tMRE3uOniphyAJhSypseL\naDGyd4LMEkRp6Tazg71T3nepb10MH4pWsvc0O5bXxxEyLeaF+1gWdN3TMALv3B3H\nqcxxFjhWQ492akdnAKYRUgGf\n-----END PRIVATE KEY-----\n",
+  "client_email": "service@dstest-1-292707.iam.gserviceaccount.com",
+  "client_id": "118117667194503067224",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://oauth2.googleapis.com/token",
+  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service%40dstest-1-292707.iam.gserviceaccount.com"
+}

BIN
api/static/.DS_Store


+ 110 - 0
api/static/gen_avatar.html

@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="en" >
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>AI ANCHOR GO</title>
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
+  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp"
+    crossorigin="anonymous">
+  <link rel="stylesheet"
+    href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt"
+    crossorigin="anonymous">
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet"> 
+  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.min.css">
+  <link rel="stylesheet" href="static/style.css">
+  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
+  <style>
+    body {
+      font-family: "Lato", sans-serif;
+    }
+    .sidenav {
+      height: 100%;
+      width: 250px;
+      position: fixed;
+      z-index: 1;
+      top: 0;
+      left: 0;
+      background: linear-gradient(to bottom, #1C7CE0, #150051);
+      overflow-x: hidden;
+      transition: 0.5s;
+      padding-top: 20px;
+    }
+    
+    .sidenav a {
+      padding: 8px 8px 8px 32px;
+      text-decoration: none;
+      font-size: 25px;
+      color: #818181;
+      display: block;
+      transition: 0.3s;
+    }
+    
+    .sidenav a:hover {
+      color: #f1f1f1;
+    }
+    
+    .sidenav .closebtn {
+      position: absolute;
+      top: 0;
+      right: 25px;
+      font-size: 36px;
+      margin-left: 50px;
+    }
+    
+    @media screen and (max-height: 450px) {
+      .sidenav {padding-top: 15px;}
+      .sidenav a {font-size: 18px;}
+    }
+    </style>
+</head>
+<body>
+  <div class="container-fluid">
+    <div id="mySidenav" class="sidenav">
+      <!-- <a href="javascript:void(0)" class="closebtn" onclick="closeNav()">&times;</a> -->
+      <h2 class="go_title">AI ANCHOR GO</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>使用說明</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>歷史紀錄</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">
+        <fieldset id='imgSrc'>
+          <h3 class="fs-subtitle" style="display: inline-block;">影像連結<img class="ms-1" src="static/img/question.png" alt="" data-bs-toggle="tooltip" data-bs-placement="right" title="僅接受png, jpg, mp4格式"></h3><br/>
+          <input type="text" name='m1' class='imgsrc imgsrc1' value="" placeholder="1" /><input id="img1" type="file" class="img_uploader img_up"><label for="img1" class="upload-btn">上傳檔案</label><br/>
+        
+          <input id="checker" type="button" class="gen_avatar action-button" value="送出" />
+
+        </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>
+  
+  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
+  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.3/jquery.easing.min.js'></script>
+
+  <script src="static/gen_avatar.js"></script>
+
+  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
+  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.min.js" integrity="sha384-Atwg2Pkwv9vp0ygtn1JAojH0nYbwNJLPhwyoVbhoPwBhjQPR5VtM2+xf0Uwh9KtT" crossorigin="anonymous"></script> 
+  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.all.min.js"></script>
+  <script src="//cdnjs.cloudflare.com/ajax/libs/validate.js/0.13.1/validate.min.js"></script>
+  <script src="static/gen_avatar.js"></script>
+
+
+  <body>
+
+</div>
+
+</body>
+</html>

+ 189 - 0
api/static/gen_avatar.js

@@ -0,0 +1,189 @@
+
+$('input[type=file]').on('change', prepareUpload);
+
+// Grab the files and set them to our variable
+function prepareUpload(event) {
+  files = event.target.files;
+  var data = new FormData();
+  //data.append('file', $('.img_up1').prop('files')[0]);
+  data.append('file', files[0]);
+  // append other variables to data if you want: data.append('field_name_x', field_value_x);
+  $(this).next().text('');
+  $(this).next().html('<img src="static/img/Spinner-1s-181px.gif">');
+  $.ajax({
+    type: 'POST',
+    processData: false, // important
+    contentType: false, // important
+    data: data,
+    url: 'uploadfile',
+    dataType: 'json',
+    success: function (jsonData) {
+      event.target.previousSibling.value =jsonData.msg;
+      $(this).prev().val(jsonData.msg);
+      event.target.nextSibling.innerHTML = '';
+      event.target.nextSibling.textContent = '上傳檔案';
+      //console.log($(this).next());
+      //$(this).next().html('上傳檔案');
+      //$(this).next().text('上傳檔案');
+    },
+    error: function (error) {
+      event.target.nextSibling.innerHTML = '';
+      event.target.nextSibling.textContent = '上傳檔案';
+      alert('圖片錯誤');
+    }
+  });
+}
+const button = document.querySelector('.next');
+
+$(".next").click(function () {
+  button.setAttribute('disabled', '');
+  setTimeout(function () {
+    button.removeAttribute('disabled')
+  }, 4000);
+  avatar = $('.avatar').val();
+  name_title = $('.title_new').val();
+  txtARR = [];
+  imgARR = [];
+  var step;
+  for (step = 1; step <= 10; step++) {
+    if ($(".txtsrc" + step).val() != "") {
+      txtARR.push($(".txtsrc" + step).val())
+    }
+  }
+  var step2;
+  for (step2 = 1; step2 <= 10; step2++) {
+    if ($(".imgsrc" + step2).val() != "") {
+      imgARR.push($(".imgsrc" + step2).val())
+    }
+  }
+  dataOBJ = { "name": name_title, "text_content": txtARR, "image_urls": imgARR, "avatar": avatar, "client_id": client_id }
+  objstr = JSON.stringify(dataOBJ);
+  console.log(dataOBJ)
+  //alert('資料已送出! 請耐心等候')
+  $.ajax({
+    url: '192.168.1.106:8887/make_anchor_video_v2',
+    //url: 'http://www.choozmo.com:8888/qqreq',
+    dataType : 'json', // 預期從server接收的資料型態
+    contentType : 'application/json; charset=utf-8', // 要送到server的資料型態
+    type: 'post',
+    data: objstr,
+    success: function(suc_data) {
+      Swal.fire({
+        title: "資料已送出",
+        icon: 'success',
+        text: `${suc_data.msg}`,
+        confirmButtonColor: '#3085d6',
+      });  
+      },
+    //data:JSON.stringify({n1:"12",n2:"22"}),
+    error: function (error) {
+      console.error(error)
+    }
+  });
+  
+  });
+
+$(".gen_avatar").click(function () {
+
+  dataOBJ = { "imgurl": $('.imgsrc').val() }
+  objstr = JSON.stringify(dataOBJ);
+  console.log(dataOBJ)
+  //alert('資料已送出! 請耐心等候')
+  $.ajax({
+    url: '192.168.1.106:8887/swapFace',
+    dataType: 'json', // 預期從server接收的資料型態
+    contentType: 'application/json; charset=utf-8', // 要送到server的資料型態
+    type: 'post',
+    data: objstr,
+    success: function (suc_data) {
+      alert(suc_data.msg)
+    },
+    //data:JSON.stringify({n1:"12",n2:"22"}),
+    error: function (error) {
+      console.error(error)
+    }
+  });
+
+});
+
+var loaded_data = ''
+function openNav() {
+  document.getElementById("mySidenav").style.width = "250px";
+  document.querySelector('.loader').style.display = "block";
+  $.get("192.168.1.106:8887/history_input", function (data, status) {
+    console.log(data)
+    loaded_data = data
+    for (var obj of data) {
+      var historyList = document.querySelector('.historyList');
+      var list = document.createElement('li');
+      list.id = obj.id;
+      // div-imgfr
+      var divImgfr = document.createElement('div');
+      divImgfr.classList.add('item_imgfr');
+      var img = document.createElement('img');
+      img.setAttribute('src', obj['image_urls'][0]);
+      divImgfr.appendChild(img);
+      // div-content
+      var contentBox = document.createElement('div');
+      contentBox.classList.add('content-box');
+      var boxTitle = document.createElement('p');
+      boxTitle.classList.add('box-title');
+      boxTitle.textContent = obj.name;
+      boxTitle.id = obj.id;
+      boxTitle.setAttribute('onclick', 'load_data()');
+
+      var boxLink = document.createElement('span');
+      boxLink.classList.add('box-link');
+      boxLink.setAttribute("data-url", obj.link);
+      boxLink.setAttribute('onclick', 'view()');
+      boxLink.innerHTML = '<i class="fas fa-play-circle me-1"></i>觀看影片';
+      contentBox.appendChild(boxTitle);
+      contentBox.appendChild(boxLink);
+      list.classList.add("historyList-item");
+      list.setAttribute('onclick', 'load_data()');
+      list.appendChild(divImgfr);
+      list.appendChild(contentBox);
+      historyList.appendChild(list);
+    }
+    document.querySelector('.loader').style.display = "none";
+  });
+}
+function closeNav() {
+  document.getElementById("mySidenav").style.width = "250px";
+}
+
+function view() {
+  event.stopPropagation();
+  console.log(event.target);
+  if(event.target.nodeName === 'I') {
+    return;
+  } else {
+    window.open(`http://${event.target.dataset.url}`, '_blank');
+  }
+}
+
+
+function load_data() {
+  var title = document.getElementById("title");
+  var linker = document.getElementById("linker");
+
+  myModal.hide()
+  tid = event.srcElement.id
+  console.log(tid);
+  linker.setAttribute('href', `http://${loaded_data.find(item => item.id == tid).link}`)
+  linker.setAttribute('target', '_blank')
+  $("#linker").html(`http://${loaded_data.find(item => item.id == tid).link}`)
+  $("#linker").show();
+  $(".linker__box").show();
+
+  $(".title_new").val(loaded_data.find(item => item.id == tid).name)
+  var step;
+  for (step = 1; step <= 10; step++) {
+    $(".txtsrc" + step).val(loaded_data.find(item => item.id == tid).text_content[step - 1])
+  }
+  var step2;
+  for (step2 = 1; step2 <= 10; step2++) {
+    $(".imgsrc" + step2).val(loaded_data.find(item => item.id == tid).image_urls[step2 - 1])
+  }
+
+}

BIN
api/static/img/Angela.webp


BIN
api/static/img/Jocelyn.webp


BIN
api/static/img/Spinner-1s-181px.gif


BIN
api/static/img/angus.webp


BIN
api/static/img/bx_loader.gif


+ 1 - 0
api/static/img/close.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="22.229" height="22.229" viewBox="0 0 22.229 22.229"><path d="M29.729,9.739,27.49,7.5l-8.876,8.876L9.739,7.5,7.5,9.739l8.876,8.876L7.5,27.49l2.239,2.239,8.876-8.876,8.876,8.876,2.239-2.239-8.876-8.876Z" transform="translate(-7.5 -7.5)" fill="#fff"/></svg>

BIN
api/static/img/girl2.png


BIN
api/static/img/ninablack.webp


BIN
api/static/img/ninawhite.webp


BIN
api/static/img/peggy.webp


BIN
api/static/img/question.png


BIN
api/static/img/stacy.webp


BIN
api/static/img/summer.webp


File diff suppressed because it is too large
+ 0 - 0
api/static/img/undraw_male_avatar_323b.svg


File diff suppressed because it is too large
+ 0 - 0
api/static/img/undraw_mobile_user_7oqo.svg


File diff suppressed because it is too large
+ 0 - 0
api/static/img/undraw_video_upload_3d4u.svg


BIN
api/static/img/wave.png


+ 240 - 0
api/static/index2.html

@@ -0,0 +1,240 @@
+<!DOCTYPE html>
+<html lang="en" >
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>AI ANCHOR GO</title>
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
+  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp"
+    crossorigin="anonymous">
+  <link rel="stylesheet"
+    href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt"
+    crossorigin="anonymous">
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet"> 
+  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.min.css">
+  <link rel="stylesheet" href="static/owl.carousel.min.css">
+  <link rel="stylesheet" href="static/owl.theme.default.min.css">
+  <link rel="stylesheet" href="static/style.css">
+  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
+  <style>
+    body {
+      font-family: "Lato", sans-serif;
+    }
+    .sidenav {
+      height: 100%;
+      width: 250px;
+      position: fixed;
+      z-index: 1;
+      top: 0;
+      left: 0;
+      background: linear-gradient(to bottom, #1C7CE0, #150051);
+      overflow-x: hidden;
+      transition: 0.5s;
+      padding-top: 20px;
+    }
+    
+    .sidenav a {
+      padding: 8px 8px 8px 32px;
+      text-decoration: none;
+      font-size: 25px;
+      color: #818181;
+      display: block;
+      transition: 0.3s;
+    }
+    
+    .sidenav a:hover {
+      color: #f1f1f1;
+    }
+    
+    .sidenav .closebtn {
+      position: absolute;
+      top: 0;
+      right: 25px;
+      font-size: 36px;
+      margin-left: 50px;
+    }
+    
+    @media screen and (max-height: 450px) {
+      .sidenav {padding-top: 15px;}
+      .sidenav a {font-size: 18px;}
+    }
+    </style>
+</head>
+<body>
+  <div class="container-fluid">
+    <div id="mySidenav" class="sidenav">
+      <!-- <a href="javascript:void(0)" class="closebtn" onclick="closeNav()">&times;</a> -->
+      <h2 class="go_title">AI ANCHOR GO</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>使用說明</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>歷史紀錄</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>預覽影片</p>
+          <i class="fas fa-link"></i>
+          <a id='linker' style="display: none;" class="ms-2">影片連結</a>
+        </div>
+        <!-- fieldsets -->
+        <fieldset>
+          <h3 class="fs-subtitle">標題<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">選擇人物<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 value="6">Angus</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="Angus" data-img="angus">
+              <div class="imgfr"><img src="static/img/angus.webp" class="card-img-top" alt="..."></div>
+              <div class="card-body">
+                <h5 class="card-title">Angus</h5>
+              </div>
+            </div>
+            <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">台詞</h3>
+          <div class="subtitle-inputs">
+          
+          </div>
+            <span class="add">+</span>
+        </fieldset>
+        <fieldset id='imgSrc'>
+          <h3 class="fs-subtitle" style="display: inline-block;">影像連結<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="checker" type="button" name="next" class="next action-button" value="送出" />
+          <h3 style="display: none;" class="fs-subtitle">處理進度</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">使用說明</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>1. 一句台詞請對應提供一個影像連結做為搭配</li>
+                      <li>2. 影像連結檔案格式支援:<stong class="strong">.png, jpg, .mp4</stong></li>
+                      <li>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">歷史紀錄</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>
+  
+  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
+  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.3/jquery.easing.min.js'></script>
+  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
+  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.min.js" integrity="sha384-Atwg2Pkwv9vp0ygtn1JAojH0nYbwNJLPhwyoVbhoPwBhjQPR5VtM2+xf0Uwh9KtT" crossorigin="anonymous"></script> 
+  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.all.min.js"></script>
+  <script src="//cdnjs.cloudflare.com/ajax/libs/validate.js/0.13.1/validate.min.js"></script>
+  <script src="static/owl.carousel.min.js"></script>
+  <script src="static/script_msg.js"></script>
+
+  <body>
+
+</div>
+
+</body>
+</html>

+ 248 - 0
api/static/index_eng.html

@@ -0,0 +1,248 @@
+<!DOCTYPE html>
+<html lang="en" >
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>AI ANCHOR GO</title>
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
+  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp"
+    crossorigin="anonymous">
+  <link rel="stylesheet"
+    href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt"
+    crossorigin="anonymous">
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet"> 
+  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.min.css">
+  <link rel="stylesheet" href="static/style.css">
+  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
+  <style>
+    body {
+      font-family: "Lato", sans-serif;
+    }
+    .sidenav {
+      height: 100%;
+      width: 250px;
+      position: fixed;
+      z-index: 1;
+      top: 0;
+      left: 0;
+      background: linear-gradient(to bottom, #1C7CE0, #150051);
+      overflow-x: hidden;
+      transition: 0.5s;
+      padding-top: 20px;
+    }
+    
+    .sidenav a {
+      padding: 8px 8px 8px 32px;
+      text-decoration: none;
+      font-size: 25px;
+      color: #818181;
+      display: block;
+      transition: 0.3s;
+    }
+    
+    .sidenav a:hover {
+      color: #f1f1f1;
+    }
+    
+    .sidenav .closebtn {
+      position: absolute;
+      top: 0;
+      right: 25px;
+      font-size: 36px;
+      margin-left: 50px;
+    }
+    
+    @media screen and (max-height: 450px) {
+      .sidenav {padding-top: 15px;}
+      .sidenav a {font-size: 18px;}
+    }
+    </style>
+</head>
+<body>
+  <div class="container-fluid">
+    <div id="mySidenav" class="sidenav">
+      <!-- <a href="javascript:void(0)" class="closebtn" onclick="closeNav()">&times;</a> -->
+      <h2 class="go_title">AI ANCHOR GO</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>使用說明</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>歷史紀錄</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>預覽影片</p>
+          <i class="fas fa-link"></i>
+          <a id='linker' style="display: none;" class="ms-2">影片連結</a>
+        </div>
+        <!-- fieldsets -->
+        <fieldset>
+          <h3 class="fs-subtitle">標題<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">選擇人物<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 value="6">Angus</option>
+            <option value="7">Peggy</option>
+            <option value="8">Stacy</option>
+            <option value="10">Nina黑</option>
+            <option value="9">Nina灰</option>
+          </select>
+          <div class="d-flex">
+            <div class="card" style="width:25%;" data-avatar="Angus" data-img="angus">
+              <div class="imgfr"><img src="static/img/angus.webp" class="card-img-top" alt="..."></div>
+              <div class="card-body">
+                <h5 class="card-title">Angus</h5>
+              </div>
+            </div>
+            <div class="card" style="width:25%;" 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" style="width:25%;" 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" style="width:25%;" 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" style="width:25%;" 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" style="width:25%;" data-avatar="Summer韓小夏" data-img="ninawhite">
+              <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>
+        </fieldset>
+        <fieldset>
+          <h3 class="fs-subtitle">台詞</h3>
+          <input type="text" name='t1' class='txtsrc txtsrc1' value="" placeholder="1"/> <br/>
+          <input type="text" name='t2' class='txtsrc txtsrc2' value="" placeholder="2" /><br/>
+          <input type="text" name='t3' class='txtsrc txtsrc3' value="" placeholder="3" /><br/>
+          <input type="text" name='t4' class='txtsrc txtsrc4' value="" placeholder="4" /><br/>
+          <input type="text" name='t5' class='txtsrc txtsrc5' value="" placeholder="5" /><br/>
+          <input type="text" name='t6' class='txtsrc txtsrc6' value="" placeholder="6" /><br/>
+          <input type="text" name='t7' class='txtsrc txtsrc7' value="" placeholder="7" /><br/>
+          <input type="text" name='t8' class='txtsrc txtsrc8' value="" placeholder="8" /><br/>
+          <input type="text" name='t9' class='txtsrc txtsrc9' value="" placeholder="9" /><br/>
+          <input type="text" name='t10' class='txtsrc txtsrc10' value="" placeholder="10" /><br/>
+        </fieldset>
+        <fieldset>
+          <h3 class="fs-subtitle">副標題</h3>
+          <input type="text" class='sub_text'  placeholder="1"/> <br/>
+          <input type="text" class='sub_text'  placeholder="2" /><br/>
+          <input type="text" class='sub_text'  placeholder="3" /><br/>
+          <input type="text" class='sub_text'  placeholder="4" /><br/>
+          <input type="text" class='sub_text'  placeholder="5" /><br/>
+          <input type="text" class='sub_text'  placeholder="6" /><br/>
+          <input type="text" class='sub_text'  placeholder="7" /><br/>
+          <input type="text" class='sub_text'  placeholder="8" /><br/>
+          <input type="text" class='sub_text'  placeholder="9" /><br/>
+          <input type="text" class='sub_text'  placeholder="10" /><br/>
+        </fieldset>
+        <fieldset id='imgSrc'>
+          <h3 class="fs-subtitle" style="display: inline-block;">影像連結<img class="ms-1" src="static/img/question.png" alt="" data-bs-toggle="tooltip" data-bs-placement="right" title="僅接受png, jpg, mp4格式"></h3><br/>
+          <input type="text" name='m1' class='imgsrc imgsrc1' value="" placeholder="1" /><input id="img1" type="file" class="img_uploader img_up"><label for="img1" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m2' class='imgsrc imgsrc2' value="" placeholder="2" /><input id="img2" type="file" class="img_uploader img_up"><label for="img2" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m3' class='imgsrc imgsrc3' value="" placeholder="3" /><input id="img3" type="file" class="img_uploader img_up"><label for="img3" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m4' class='imgsrc imgsrc4' value="" placeholder="4" /><input id="img4" type="file" class="img_uploader img_up"><label for="img4" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m5' class='imgsrc imgsrc5' value="" placeholder="5" /><input id="img5" type="file" class="img_uploader img_up"><label for="img5" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m6' class='imgsrc imgsrc6' value="" placeholder="6" /><input id="img6" type="file" class="img_uploader img_up"><label for="img6" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m7' class='imgsrc imgsrc7' value="" placeholder="7" /><input id="img7" type="file" class="img_uploader img_up"><label for="img7" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m8' class='imgsrc imgsrc8' value="" placeholder="8" /><input id="img8" type="file" class="img_uploader img_up"><label for="img8" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m9' class='imgsrc imgsrc9' value="" placeholder="9" /><input id="img9" type="file" class="img_uploader img_up"><label for="img9" class="upload-btn">上傳檔案</label><br/>
+          <input type="text" name='m10' class='imgsrc imgsrc10' value="" placeholder="10" /><input id="img10" type="file" class="img_uploader img_up"><label for="img10" class="upload-btn">上傳檔案</label><br/>
+          <input id="checker" type="button" name="next" class="next action-button" value="送出" />
+          <h3 style="display: none;" class="fs-subtitle">處理進度</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">使用說明</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>1. 一句台詞請對應提供一個影像連結做為搭配</li>
+                      <li>2. 影像連結檔案格式支援:<stong class="strong">.png, jpg, .mp4</stong></li>
+                      <li>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">歷史紀錄</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>
+  
+  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
+  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.3/jquery.easing.min.js'></script>
+  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
+  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.min.js" integrity="sha384-Atwg2Pkwv9vp0ygtn1JAojH0nYbwNJLPhwyoVbhoPwBhjQPR5VtM2+xf0Uwh9KtT" crossorigin="anonymous"></script> 
+  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.all.min.js"></script>
+  <script src="//cdnjs.cloudflare.com/ajax/libs/validate.js/0.13.1/validate.min.js"></script>
+  <script src="static/script_anchor_eng.js"></script>
+
+  <body>
+
+</div>
+
+</body>
+</html>

+ 164 - 0
api/static/lan.js

@@ -0,0 +1,164 @@
+//設定cookie
+function setCookie(name,value)
+{
+    var Days = 30;
+    var exp = new Date();
+    exp.setTime(exp.getTime() + Days*24*60*60*1000);
+    document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
+}
+
+//獲取cookie
+function getCookie(name)
+{
+    var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
+    if(arr=document.cookie.match(reg))
+    return unescape(arr[2]);
+    else
+    return null;
+}
+
+var zh = {
+    "make_video" : "製作影片",
+    "make_slides" : "影片製作(投影片版本)",
+    "login" : "登入",
+    "user_profile": "會員資料",
+    "logout": "登出",
+    "en": "英文",
+    "zh": "中文",
+    "choose_character": "選擇人物",
+    "usage_intro": "使用說明",
+    "history": "歷史紀錄",
+    "preview_videos": "預覽影片",
+    "video_link":  "影片連結",
+    "video_title": "標題",
+    "p_choose_character": "請選擇人物",
+    "lines": "台詞",
+    "img_link": "影像連結",
+    "processing_progress": "處理進度",
+    "usage_intro": "使用說明",
+    "one_line_to_one_img": "1. 一句台詞請對應提供一個影像連結做為搭配",
+    "sup_img_profile": "2. 影像連結檔案格式支援:",
+    "submit_to_wait": "3. 點選“送出”之後需等待一段影片製作的時間,請您耐心等候,待製作完畢可於通知網址查看",
+    "add_eng": "加入英文:",
+    "submut": "送出",
+    "privacy_term": "同意隱私政策及使用條款"
+};
+
+var en = {
+    "make_video" : "Make Videos",
+    "make_slides" : "Make Videos By Slides",
+    "login" : "Login",
+    "user_profile": "User Profile",
+    "logout": "Logout",
+    "en": "English",
+    "zh": "Chinese",
+    "choose_character": "Choose Character",
+    "usage_intro": "Usage Introduction",
+    "history": "History",
+    "preview_videos": "Preview Videos",
+    "video_link":  "Video Link",
+    "video_title": "Video Title",
+    "p_choose_character": "Choose Character",
+    "lines": "Lines",
+    "img_link": "Image Link",
+    "processing_progress": "Processing Progress",
+    "usage_intro": "Usage Introduction",
+    "one_line_to_one_img": "1. Please Provide an Image Link Corresponding to a Line as a Collocation",
+    "sup_img_profile": "2. Support File Format:",
+    "submit_to_wait": "3. After Clicking Submit, You Will Need To Wait For A Period Of Time For The Production Of The Video. Please Wait Patiently. You Can Check It At The Notification URL When The Production Is Completed.",
+    "add_eng": "Allow English Lines:",
+    "submut": "Submit",
+    "privacy_term": "I Agree to privacy policy and terms of use."
+};
+
+
+// 4. 轉換
+function changeLan(val) {
+    var val = val.value;
+    console.log(`val: ${val}`);
+    setCookie('lan', val);
+    $('[set-lan]').each(function(){
+        var me = $(this);
+        var a = me.attr('set-lan').split(':');
+        var p = a[0];   //文字放置位置
+        var m = a[1];   //文字標示
+        
+        //用虎選擇語言後保存在cookie中,這裡讀取cooikes的語言版本
+        var lan = getCookie('lan');
+        console.log(lan);
+    
+        //選取語言文字
+        switch(lan){
+            case 'zh':
+                var t = zh[m];  
+                console.log(t);
+                break;
+            case 'en':
+                var t = en[m];
+                console.log(t);
+                break;
+            default:
+                var t = zh[m];
+                console.log(t);
+        }
+    
+        //所選語言沒有就換
+        if(t==undefined) t = en[m];
+        if(t==undefined) t = zh[m];
+    
+        if(t==undefined) return true;   //都沒有就跳出
+    
+        //文字放置位置有(html,val等,可以自己添加)
+        switch(p){
+            case 'html':
+                me.html(t);
+                break;
+            case 'placeholder':
+                me.placeholder(t);
+                console.log("placeholder");
+                console.log(t);
+            case 'val':
+            case 'value':
+                me.val(t);
+                break;
+            default:
+                me.html(t);
+        }
+    
+    });
+}
+
+
+
+// js裡面的轉換
+function get_lan(m)
+{
+    //獲取文字
+    var lan = getCookie('lan');     //語言版本
+    //選取語言文字
+    switch(lan){
+        case 'zh':
+            var t = zh[m];
+            break;
+        case 'hk':
+            var t = hk[m];
+            break;
+        default:
+            var t = en[m];
+    }
+
+    //如果沒有找到就轉換其他語言
+    if(t==undefined) t = zh[m];
+    if(t==undefined) t = en[m];
+    if(t==undefined) t = hk[m];
+
+    if(t==undefined) t = m; //如果還是沒有就直接用標示
+
+    return t;
+}
+
+
+// 預設中文版
+var lan = getCookie('lan');
+console.log(`目前語言版本: ${lan}`);
+changeLan(lan);

File diff suppressed because it is too large
+ 5 - 0
api/static/owl.carousel.min.css


File diff suppressed because it is too large
+ 5 - 0
api/static/owl.carousel.min.js


+ 6 - 0
api/static/owl.theme.default.min.css

@@ -0,0 +1,6 @@
+/**
+ * Owl Carousel v2.3.4
+ * Copyright 2013-2018 David Deutsch
+ * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE
+ */
+.owl-theme .owl-dots,.owl-theme .owl-nav{text-align:center;-webkit-tap-highlight-color:transparent}.owl-theme .owl-nav{margin-top:10px}.owl-theme .owl-nav [class*=owl-]{color:#FFF;font-size:14px;margin:5px;padding:4px 7px;background:#D6D6D6;display:inline-block;cursor:pointer;border-radius:3px}.owl-theme .owl-nav [class*=owl-]:hover{background:#869791;color:#FFF;text-decoration:none}.owl-theme .owl-nav .disabled{opacity:.5;cursor:default}.owl-theme .owl-nav.disabled+.owl-dots{margin-top:10px}.owl-theme .owl-dots .owl-dot{display:inline-block;zoom:1}.owl-theme .owl-dots .owl-dot span{width:10px;height:10px;margin:5px 7px;background:#D6D6D6;display:block;-webkit-backface-visibility:visible;transition:opacity .2s ease;border-radius:30px}.owl-theme .owl-dots .owl-dot.active span,.owl-theme .owl-dots .owl-dot:hover span{background:#869791}

+ 241 - 0
api/static/script_anchor_eng.js

@@ -0,0 +1,241 @@
+var client_id = Date.now()
+var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
+var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
+  return new bootstrap.Tooltip(tooltipTriggerEl)
+});
+var myModal = new bootstrap.Modal(document.getElementById('history'), {
+  keyboard: false
+})
+var avatarModal = new bootstrap.Modal(document.getElementById('avatarmega'), {
+  keyboard: false
+})
+var modalImg = document.querySelector("#avatarmega .modal-img");
+var modalTitle = document.querySelector("#avatarmega .modal-title");
+var avatarSelector = document.getElementById("avatar");
+var card = document.getElementsByClassName('card');
+card = [... card];
+avatarSelector.addEventListener('change', avatarChange);
+avatarChange();
+
+function addCardListener() {
+  for(let i = 0;i < card.length; i++){
+    card[i].addEventListener('click', openavatarModel);
+  }
+}
+
+addCardListener();
+
+function avatarChange() {
+  var value = avatarSelector.options[avatarSelector.selectedIndex].text;
+  console.log(value);
+  for(let i = 0;i < card.length; i++) {
+    if(card[i].dataset.avatar == value) {
+      card[i].classList.add('active');
+    } else {
+      card[i].classList.remove('active');
+    }
+  }
+}
+
+function openavatarModel() {
+  console.log(this.dataset.img);
+  modalImg.setAttribute("src", `static/img/${this.dataset.img}.webp`);
+  modalTitle.textContent = `${this.dataset.avatar}`;
+  avatarModal.show();
+}
+
+$('input[type=file]').on('change', prepareUpload);
+
+// Grab the files and set them to our variable
+function prepareUpload(event) {
+  files = event.target.files;
+  var data = new FormData();
+  //data.append('file', $('.img_up1').prop('files')[0]);
+  data.append('file', files[0]);
+  // append other variables to data if you want: data.append('field_name_x', field_value_x);
+  $(this).next().text('');
+  $(this).next().html('<img src="static/img/Spinner-1s-181px.gif">');
+  $.ajax({
+    type: 'POST',
+    processData: false, // important
+    contentType: false, // important
+    data: data,
+    url: '192.168.1.106:8887/uploadfile',
+    dataType: 'json',
+    success: function (jsonData) {
+      event.target.previousSibling.value =jsonData.msg;
+      $(this).prev().val(jsonData.msg);
+      event.target.nextSibling.innerHTML = '';
+      event.target.nextSibling.textContent = '上傳檔案';
+      //console.log($(this).next());
+      //$(this).next().html('上傳檔案');
+      //$(this).next().text('上傳檔案');
+    },
+    error: function (error) {
+      event.target.nextSibling.innerHTML = '';
+      event.target.nextSibling.textContent = '上傳檔案';
+      alert('圖片錯誤');
+    }
+  });
+}
+const button = document.querySelector('.next');
+
+$(".next").click(function () {
+  button.setAttribute('disabled', '');
+  setTimeout(function () {
+    button.removeAttribute('disabled')
+  }, 4000);
+  avatar = $('.avatar').val();
+  name_title = $('.title_new').val();
+  txtARR = [];
+  imgARR = [];
+  subtitleARR = [];
+  var step;
+  for (step = 1; step <= 10; step++) {
+    if ($(".txtsrc" + step).val() != "") {
+      txtARR.push($(".txtsrc" + step).val())
+    }
+  }
+  var step2;
+  for (step2 = 1; step2 <= 10; step2++) {
+    if ($(".imgsrc" + step2).val() != "") {
+      imgARR.push($(".imgsrc" + step2).val())
+    }
+  }
+  for (let i = 0; i < 10; i++) {
+    var stitles = document.getElementsByClassName('sub_text')[i].value
+    if (stitles != "") {
+      subtitleARR.push(stitles)
+    }
+  }
+  dataOBJ = { "name": name_title, "text_content": txtARR, "image_urls": imgARR, "sub_titles":subtitleARR, "avatar": avatar, "client_id": client_id }
+  objstr = JSON.stringify(dataOBJ);
+  console.log(dataOBJ)
+  //alert('資料已送出! 請耐心等候')
+  $.ajax({
+    url: '192.168.1.106:8887/make_anchor_video_eng',
+    //url: 'http://www.choozmo.com:8888/qqreq',
+    dataType : 'json', // 預期從server接收的資料型態
+    contentType : 'application/json; charset=utf-8', // 要送到server的資料型態
+    type: 'post',
+    data: objstr,
+    success: function(suc_data) {
+      Swal.fire({
+        title: "資料已送出",
+        icon: 'success',
+        text: `${suc_data.msg}`,
+        confirmButtonColor: '#3085d6',
+      });  
+      },
+    //data:JSON.stringify({n1:"12",n2:"22"}),
+    error: function (error) {
+      console.error(error)
+    }
+  });
+  
+  });
+
+$(".gen_avatar").click(function () {
+
+  dataOBJ = { "imgurl": $('.img_src').val() }
+  objstr = JSON.stringify(dataOBJ);
+  console.log(dataOBJ)
+  //alert('資料已送出! 請耐心等候')
+  $.ajax({
+    url: '192.168.1.106:8887/swapFace',
+    dataType: 'json', // 預期從server接收的資料型態
+    contentType: 'application/json; charset=utf-8', // 要送到server的資料型態
+    type: 'post',
+    data: objstr,
+    success: function (suc_data) {
+      alert(suc_data.msg)
+    },
+    //data:JSON.stringify({n1:"12",n2:"22"}),
+    error: function (error) {
+      console.error(error)
+    }
+  });
+
+});
+
+var loaded_data = ''
+function openNav() {
+  document.getElementById("mySidenav").style.width = "250px";
+  document.querySelector('.loader').style.display = "block";
+  $.get("192.168.1.106:8887/history_input", function (data, status) {
+    console.log(data)
+    loaded_data = data
+    for (var obj of data) {
+      var historyList = document.querySelector('.historyList');
+      var list = document.createElement('li');
+      list.id = obj.id;
+      // div-imgfr
+      var divImgfr = document.createElement('div');
+      divImgfr.classList.add('item_imgfr');
+      var img = document.createElement('img');
+      img.setAttribute('src', obj['image_urls'][0]);
+      divImgfr.appendChild(img);
+      // div-content
+      var contentBox = document.createElement('div');
+      contentBox.classList.add('content-box');
+      var boxTitle = document.createElement('p');
+      boxTitle.classList.add('box-title');
+      boxTitle.textContent = obj.name;
+      boxTitle.id = obj.id;
+      boxTitle.setAttribute('onclick', 'load_data()');
+
+      var boxLink = document.createElement('span');
+      boxLink.classList.add('box-link');
+      boxLink.setAttribute("data-url", obj.link);
+      boxLink.setAttribute('onclick', 'view()');
+      boxLink.innerHTML = '<i class="fas fa-play-circle me-1"></i>觀看影片';
+      contentBox.appendChild(boxTitle);
+      contentBox.appendChild(boxLink);
+      list.classList.add("historyList-item");
+      list.setAttribute('onclick', 'load_data()');
+      list.appendChild(divImgfr);
+      list.appendChild(contentBox);
+      historyList.appendChild(list);
+    }
+    document.querySelector('.loader').style.display = "none";
+  });
+}
+function closeNav() {
+  document.getElementById("mySidenav").style.width = "250px";
+}
+
+function view() {
+  event.stopPropagation();
+  console.log(event.target);
+  if(event.target.nodeName === 'I') {
+    return;
+  } else {
+    window.open(`http://${event.target.dataset.url}`, '_blank');
+  }
+}
+
+
+function load_data() {
+  var title = document.getElementById("title");
+  var linker = document.getElementById("linker");
+
+  myModal.hide()
+  tid = event.srcElement.id
+  console.log(tid);
+  linker.setAttribute('href', `http://${loaded_data.find(item => item.id == tid).link}`)
+  linker.setAttribute('target', '_blank')
+  $("#linker").html(`http://${loaded_data.find(item => item.id == tid).link}`)
+  $("#linker").show();
+  $(".linker__box").show();
+
+  $(".title_new").val(loaded_data.find(item => item.id == tid).name)
+  var step;
+  for (step = 1; step <= 10; step++) {
+    $(".txtsrc" + step).val(loaded_data.find(item => item.id == tid).text_content[step - 1])
+  }
+  var step2;
+  for (step2 = 1; step2 <= 10; step2++) {
+    $(".imgsrc" + step2).val(loaded_data.find(item => item.id == tid).image_urls[step2 - 1])
+  }
+
+}

+ 471 - 0
api/static/script_util.js

@@ -0,0 +1,471 @@
+var client_id = Date.now()
+var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
+var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
+  return new bootstrap.Tooltip(tooltipTriggerEl)
+});
+var myModal = new bootstrap.Modal(document.getElementById('history'), {
+  keyboard: false
+})
+var avatarModal = new bootstrap.Modal(document.getElementById('avatarmega'), {
+  keyboard: false
+})
+var modalImg = document.querySelector("#avatarmega .modal-img");
+var modalTitle = document.querySelector("#avatarmega .modal-title");
+var avatarSelector = document.getElementById("avatar");
+var card = document.getElementsByClassName('card');
+card = [...card];
+avatarSelector.addEventListener('change', avatarChange);
+avatarChange();
+
+function addCardListener() {
+  for (let i = 0; i < card.length; i++) {
+    card[i].addEventListener('click', openavatarModel);
+  }
+}
+
+addCardListener();
+
+function avatarChange() {
+  var value = avatarSelector.options[avatarSelector.selectedIndex].text;
+  $('.owl-carousel').trigger('to.owl.carousel', avatarSelector.selectedIndex);
+  console.log(value);
+  for (let i = 0; i < card.length; i++) {
+    card[i].classList.remove('active');
+    if (card[i].dataset.avatar == value) {
+      card[i].classList.add('active');
+    }
+  }
+}
+
+function openavatarModel() {
+  console.log(this.dataset.img);
+  modalImg.setAttribute("src", `static/img/${this.dataset.img}.webp`);
+  modalTitle.textContent = `${this.dataset.avatar}`;
+  avatarModal.show();
+}
+
+$('input[type=file]').on('change', prepareUpload);
+function prepareUpload(event) {
+  files = event.target.files;
+  var data = new FormData();
+  //data.append('file', $('.img_up1').prop('files')[0]);
+  data.append('file', files[0]);
+  // append other variables to data if you want: data.append('field_name_x', field_value_x);
+  $(this).next().text('');
+  $(this).next().html('<img src="static/img/Spinner-1s-181px.gif">');
+  $.ajax({
+    type: 'POST',
+    processData: false, // important
+    contentType: false, // important
+    data: data,
+    url: '/uploadfile',
+    dataType: 'json',
+    success: function (jsonData) {
+      event.target.previousSibling.value = jsonData.msg;
+      $(this).prev().val(jsonData.msg);
+      event.target.nextSibling.innerHTML = '';
+      event.target.nextSibling.textContent = '上傳檔案';
+      //console.log($(this).next());
+      //$(this).next().html('上傳檔案');
+      //$(this).next().text('上傳檔案');
+    },
+    error: function (error) {
+      event.target.nextSibling.innerHTML = '';
+      event.target.nextSibling.textContent = '上傳檔案';
+      alert('圖片錯誤');
+    }
+  });
+}
+const button = document.querySelector('.next');
+
+$(".next").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");
+  xhr.setRequestHeader("accept", "application/json");
+  xhr.setRequestHeader("Authorization","Bearer "+jwt_token)
+  xhr.setRequestHeader("Content-Type", "application/json");
+  xhr.onreadystatechange = function () {
+    if (xhr.readyState === 4) {
+      responseOBJ = JSON.parse(xhr.responseText)
+      if (responseOBJ.msg=='ok')
+      {
+        Swal.fire({
+          title: "資料已送出",
+          icon: 'success',
+          text: '資料已傳送,請耐心等候',
+          confirmButtonColor: '#3085d6',
+        });
+      }
+      else{
+        Swal.fire({
+          title: "發生錯誤",
+          icon: 'error',
+          text: responseOBJ.msg,
+          confirmButtonColor: '#3085d6',
+        });
+      }
+      
+    }
+  };
+  var data = renderXHR_data(dataOBJ)
+  console.log(data)
+  result = xhr.send(objstr);
+});
+const buttonSend = document.querySelector('#sendBTN');
+$("#sendBTN").click(function () {
+  buttonSend.setAttribute('disabled', '');
+  setTimeout(function () {
+    buttonSend.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) {
+      responseOBJ = JSON.parse(xhr.responseText)
+      if (responseOBJ.msg=='ok')
+      {
+        Swal.fire({
+          title: "資料已送出",
+          icon: 'success',
+          text: '資料已傳送,請耐心等候',
+          confirmButtonColor: '#3085d6',
+        });
+      }
+      else{
+        Swal.fire({
+          title: "發生錯誤",
+          icon: 'error',
+          text: responseOBJ.msg,
+          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', '');
+  setTimeout(function () {
+    slide_button.removeAttribute('disabled')
+  }, 4000);
+  avatar = $('.avatar').val();
+  var step;
+  multiLang = 0
+  if ($('#multiLang').prop("checked")) {multiLang = 1;}
+  dataOBJ = {'slide_url':$('#slide_raw_url').val(),"avatar": avatar,"multiLang":multiLang, "client_id": client_id }
+  objstr = JSON.stringify(dataOBJ);
+  jwt_token =  get_jwt_token()
+  var xhr = new XMLHttpRequest();
+  xhr.open("POST", "/make_anchor_video_gSlide");
+  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);
+});
+
+$(".gen_avatar").click(function () {
+  dataOBJ = { "imgurl": $('.img_src').val() }
+  objstr = JSON.stringify(dataOBJ);
+  console.log(dataOBJ)
+  //alert('資料已送出! 請耐心等候')
+  $.ajax({
+    url: '/swapFace',
+    dataType: 'json', // 預期從server接收的資料型態
+    contentType: 'application/json; charset=utf-8', // 要送到server的資料型態
+    type: 'post',
+    data: objstr,
+    success: function (suc_data) {
+      alert(suc_data.msg)
+    },
+    //data:JSON.stringify({n1:"12",n2:"22"}),
+    error: function (error) {
+      console.error(error)
+    }
+  });
+});
+
+var loaded_data = ''
+function openNav() {
+  document.getElementById("mySidenav").style.width = "250px";
+  document.querySelector('.loader').style.display = "block";
+  $.get("/history_input", function (data, status) {
+    console.log(data)
+    loaded_data = data
+    for (var obj of data) {
+      var historyList = document.querySelector('.historyList');
+      var list = document.createElement('li');
+      list.id = obj.id;
+      // div-imgfr
+      var divImgfr = document.createElement('div');
+      divImgfr.classList.add('item_imgfr');
+      var img = document.createElement('img');
+      img.setAttribute('src', obj['image_urls'][0]);
+      divImgfr.appendChild(img);
+      // div-content
+      var contentBox = document.createElement('div');
+      contentBox.classList.add('content-box');
+      var boxTitle = document.createElement('p');
+      boxTitle.classList.add('box-title');
+      boxTitle.textContent = obj.name;
+      boxTitle.id = obj.id;
+      boxTitle.setAttribute('onclick', 'load_data()');
+
+      var boxLink = document.createElement('span');
+      boxLink.classList.add('box-link');
+      boxLink.setAttribute("data-url", obj.link);
+      boxLink.setAttribute('onclick', 'view()');
+      boxLink.innerHTML = '<i class="fas fa-play-circle me-1"></i>觀看影片';
+      contentBox.appendChild(boxTitle);
+      contentBox.appendChild(boxLink);
+      list.classList.add("historyList-item");
+      list.setAttribute('onclick', 'load_data()');
+      list.appendChild(divImgfr);
+      list.appendChild(contentBox);
+      historyList.appendChild(list);
+    }
+    document.querySelector('.loader').style.display = "none";
+  });
+}
+function closeNav() {
+  document.getElementById("mySidenav").style.width = "250px";
+}
+
+function view() {
+  event.stopPropagation();
+  console.log(event.target);
+  if (event.target.nodeName === 'I') {
+    return;
+  } else {
+    window.open(`http://${event.target.dataset.url}`, '_blank');
+  }
+}
+
+function renderXHR_data(jsonObj) {
+  XHRstring = ''
+  for (const [key, value] of Object.entries(jsonObj)) {
+    console.log(value)
+    if (typeof (value) == "object") {
+      XHRstring += (key+'=['+value.join(',')+']&')
+    }
+    else {
+      XHRstring += (key + '=' + value + '&')
+    }
+  }
+  XHRstring = XHRstring.substring(0, XHRstring.length - 1);
+  return XHRstring
+}
+
+function get_jwt_token(){
+  jwt_raw = document.cookie.split(';').filter(s=>s.includes('jwt_token'))[0]
+  return jwt_raw.split('=')[1]
+}
+
+function load_data() {
+  var title = document.getElementById("title");
+  var linker = document.getElementById("linker");
+
+  myModal.hide()
+  tid = event.srcElement.id
+  console.log(tid);
+  linker.setAttribute('href', `http://${loaded_data.find(item => item.id == tid).link}`)
+  linker.setAttribute('target', '_blank')
+  $("#linker").html(`http://${loaded_data.find(item => item.id == tid).link}`)
+  $("#linker").show();
+  $(".linker__box").show();
+
+  let historyItem = loaded_data.filter(item => item.id == tid)[0];
+  console.log(historyItem);
+  $(".title_new").val(loaded_data.find(item => item.id == tid).name);
+
+  let txtlength = historyItem.text_content.length;
+  let imglength = historyItem.image_urls.length;
+
+  subtitleInputs.innerHTML = '';
+  imgInputs.innerHTML = '';
+  for (let i = 0; i < txtlength; i++) {
+    var txtinput = document.createElement("input");
+    txtinput.setAttribute('type', 'text');
+    txtinput.setAttribute('name', `t${i + 1}`);
+    txtinput.value = historyItem.text_content[i];
+    txtinput.setAttribute('placeholder', `${i + 1}`);
+    txtinput.classList.add('txtsrc', `txtsrc${i + 1}`)
+    subtitleInputs.appendChild(txtinput);
+  }
+  for (let i = 0; i < imglength; i++) {
+    var imginput = document.createElement("input");
+    imginput.setAttribute('type', 'text');
+    imginput.setAttribute('name', `m${i + 1}`);
+    imginput.classList.add('imgsrc', `imgsrc${i + 1}`);
+    imginput.value = historyItem.image_urls[i];
+    imginput.setAttribute('placeholder', `${i + 1}`);
+    imgInputs.appendChild(imginput);
+
+    var imgupload = document.createElement("input");
+    imgupload.setAttribute('id', `img${i + 1}`);
+    imgupload.setAttribute('type', `file`);
+    imgupload.classList.add('img_uploader', 'img_up');
+    imgInputs.appendChild(imgupload);
+    var imguploadlabel = document.createElement("label");
+    imguploadlabel.setAttribute('for', `img${i + 1}`);
+    imguploadlabel.classList.add('upload-btn');
+    imguploadlabel.textContent = '上傳檔案';
+    imgInputs.appendChild(imguploadlabel);
+  }
+
+}
+
+
+var subtitleInputs = document.querySelector(".subtitle-inputs");
+var imgInputs = document.querySelector(".img-inputs");
+let length = 5;
+function initial() {
+  for (let i = 0; i < length; i++) {
+    rendertxtBlock(i + 1);
+    renderimgBlock(i + 1);
+  }
+  console.log(document.querySelectorAll(".txtsrc").length + 1);
+}
+
+initial();
+
+var addbtn = document.querySelector(".add");
+var addimgbtn = document.querySelector(".addimg");
+
+addbtn.addEventListener('click', addtxtBlock);
+addimgbtn.addEventListener('click', addimgBlock);
+
+function addtxtBlock() {
+  let newIdx = document.querySelectorAll(".txtsrc").length + 1;
+  rendertxtBlock(newIdx);
+}
+
+function addimgBlock() {
+  let newimgIdx = document.querySelectorAll(".imgsrc").length + 1;
+  renderimgBlock(newimgIdx);
+}
+
+function rendertxtBlock(i) {
+  var txtinput = document.createElement("input");
+  txtinput.setAttribute('type', 'text');
+  txtinput.setAttribute('name', `t${i}`);
+  txtinput.value = "";
+  txtinput.setAttribute('placeholder', `${i}`);
+  txtinput.classList.add('txtsrc', `txtsrc${i}`)
+  subtitleInputs.appendChild(txtinput);
+}
+
+function renderimgBlock(i) {
+  var imginput = document.createElement("input");
+  imginput.setAttribute('type', 'text');
+  imginput.setAttribute('name', `m${i}`);
+  imginput.classList.add('imgsrc', `imgsrc${i}`);
+  imginput.value = "";
+  imginput.setAttribute('placeholder', `${i}`);
+  imgInputs.appendChild(imginput);
+
+  var imgupload = document.createElement("input");
+  imgupload.setAttribute('id', `img${i}`);
+  imgupload.setAttribute('type', `file`);
+  imgupload.classList.add('img_uploader', 'img_up');
+  imgInputs.appendChild(imgupload);
+  var imguploadlabel = document.createElement("label");
+  imguploadlabel.setAttribute('for', `img${i}`);
+  imguploadlabel.classList.add('upload-btn');
+  imguploadlabel.textContent = '上傳檔案';
+  imgInputs.appendChild(imguploadlabel);
+  $('input[type=file]').on('change', prepareUpload);
+}
+
+
+$('.owl-carousel').owlCarousel({
+  loop: true,
+  margin: 10,
+  nav: false,
+  mouseDrag: true,
+  touchDrag: true,
+  smartSpeed: 1000,
+  autoplay: true,
+  autoplayTimeout: 8000,
+  autoplayHoverPause: false,
+  responsive: {
+    0: {
+      items: 1
+    },
+    600: {
+      items: 2
+    },
+    1000: {
+      items: 4
+    }
+  }
+})

+ 925 - 0
api/static/style.css

@@ -0,0 +1,925 @@
+@charset "UTF-8"; 
+/*custom font*/
+@import url(https://fonts.googleapis.com/css?family=Montserrat);
+
+/*basic reset*/
+* {margin: 0; padding: 0;}
+
+html {
+	min-height: 100%;
+	height: auto;
+	/*Image only BG fallback*/
+	
+	/*background = gradient + image pattern combo*/
+	/* background: 
+		linear-gradient(rgba(196, 102, 0, 0.6), rgba(155, 89, 182, 0.6)); */
+}
+
+body {
+	font-family: 'Montserrat', sans-serif;
+	background-color: white;
+	font-size: 1.05rem;
+}
+
+.ml {
+	margin-left: 10px;
+}
+
+.mr {
+	margin-right: 10px;
+}
+
+.mb {
+	margin-bottom: 12px;
+}
+
+.top {
+	margin-top: 20px;
+	position: sticky;
+	top: 0;
+	left: 50%;
+	z-index: 20;
+	background-color: inherit;
+}
+
+.navbar {
+	background-color: white;
+}
+
+/* .imf {
+	position: fixed;
+	top: 0;
+	left: 50%;
+	transform: translateX(-50%);
+	z-index: 3;
+} */
+
+.img_banner {
+	background-image: url('images/banner_top1.jpg');
+	background-repeat: no-repeat;
+	background-size: contain;
+	background-position: center;
+	width: 100%;
+	height: 65px;
+}
+
+/*form styles*/
+.img_logo {
+	display: inline-block;
+	margin: 0 auto;
+	margin-bottom: 10px;
+}
+
+.title__block {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	margin: 0px auto;
+}
+
+.sub-logo {
+	display: inline-block;
+	font-weight: 700;
+	font-size: 1.4rem;
+	color: white;
+}
+
+.slogan {
+	display: block;
+	font-weight: 700;
+	font-size: 1.2rem;
+	color: white;
+}
+
+.notice_card {
+	box-sizing: border-box;
+	width: 80%;
+	margin: 0 10%;
+	margin-bottom: 1.5rem;
+	background: white;
+	border: 0 none;
+	border-radius: 3px;
+	box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.4);
+	padding: 10px 20px;
+	line-height: 1.5rem;
+}
+#myProgress {
+	width: 100%;
+	background-color: #ddd;
+  }
+  
+  #myBar {
+	width: 10%;
+	height: 30px;
+	background-color: #1C7CE0;
+	text-align: center;
+	line-height: 30px;
+	color: white;
+  }
+
+.go_title {
+	color: white;
+	font-family: 'Montserrat', sans-serif;
+	padding: 1rem;
+}
+
+.nav-list {
+	color: white;
+	margin-top: 3rem;
+}
+
+.nav-list-item {
+	font-size: 1.2rem;
+	cursor: pointer;
+	position: relative;
+
+}
+
+.nav-list-item::after {
+	content: " ";
+	position: absolute;
+	width: 100%;
+	height: 100%;
+	left: 0;
+	top: 0;
+	border-bottom: 1px solid white;
+	opacity: 0;
+	transition: all .4s;
+}
+
+.nav-list-item:hover::after {
+
+	opacity: 1;
+}
+
+.right-text {
+	position: absolute;
+	bottom: 0rem;
+}
+
+.loader {
+	position: absolute;
+	top: 30%;
+	left: 45%;
+}
+
+.imgfr {
+	width:90px;
+	height: 90px;
+	border-radius: 50%;
+	overflow: hidden;
+	margin: 1rem auto 0 auto;
+}
+
+.card-title {
+	font-family: 'Montserrat', sans-serif;
+}
+
+.strong {
+	font-weight: 600;
+}
+
+.card {
+	border: none;
+	position: relative;
+}
+
+.card.active {
+	border: 1px solid gray;
+}
+
+.card.active::before {
+	position: absolute;
+	content: "✓";
+	bottom: 0;
+	right: 5px;
+}
+
+.zoomin {
+	display: none;
+	position: absolute;
+	bottom: 0;
+	left: 0;
+}
+
+.script_err {
+	display: none;
+}
+/* .history-modal {
+	height: 80vh;
+}
+.history-modal .modal-content{
+	height: 80vh;
+	
+}
+.history-modal .modal-conten .modal-terms {
+	height: 90%;
+	overflow: scroll;
+} */
+
+.historyList-item {
+	padding: 1rem;
+	display: block;
+	width: 80%;
+	margin: 1rem auto;
+	border: 1px solid rgb(177, 177, 177);
+	border-radius: 10px;
+	display: flex;
+	font-size: .9rem;
+}
+
+.historyList-link {
+	text-decoration: none;
+	color: rgb(73, 73, 73);
+}
+
+.historyList-link:hover {
+	color: rgb(124, 124, 124);
+}
+
+.content {
+	width: calc(100% - 250px);
+}
+/*form styles*/
+#msform {
+	max-width: 1000px;
+	min-width: 370px;
+	margin: 50px auto;
+	margin-bottom: 20px;
+	text-align: center;
+	position: relative;
+}
+#msform fieldset {
+	background: white;
+	border: 0 none;
+	border-radius: 3px;
+	box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.4);
+	padding: 20px 30px;
+	box-sizing: border-box;
+	width: 80%;
+	margin: 3rem 10%;
+	
+	/*stacking fieldsets above each other*/
+	position: relative;
+}
+/*Hide all except first fieldset*/
+
+/*inputs*/
+#msform input[type="text"], #msform textarea, #msform input[type="email"] {
+	padding: 15px;
+	border: 1px solid #ccc;
+	border-radius: 3px;
+	margin-bottom: 16px;
+	width: 100%;
+	box-sizing: border-box;
+	font-family: montserrat;
+	color: #2C3E50;
+	font-size: 14px;
+}
+/*buttons*/
+#msform .action-button {
+	width: 100px;
+	background: #1C7CE0;
+	font-weight: bold;
+	color: white;
+	border: 0 none;
+	border-radius: 1px;
+	cursor: pointer;
+	padding: 10px 5px;
+	margin: 10px auto;
+	display: block;
+}
+#msform .action-button:hover, #msform .action-button:focus {
+	box-shadow: 0 0 0 2px white, 0 0 0 3px #1C7CE0;
+}
+#msform .next[disabled] {
+	background-color: grey;
+}
+/*headings*/
+.fs-title {
+	font-size: 15px;
+	text-transform: uppercase;
+	color: #2C3E50;
+	margin-bottom: 10px;
+}
+.fs-subtitle {
+	display: inline-block;
+	font-weight: normal;
+	font-size: 18px;
+	color: #666;
+	margin-bottom: 20px;
+	padding-bottom: 3px;
+	border-bottom: 2px solid #1C7CE0;
+	margin-left: 2rem;
+	margin-right: 2rem;
+}
+.fs-label {
+	display: block;
+	text-align: left;
+	margin-bottom: 8px;
+}
+.fs-label > i {
+	margin-right: 3px;
+}
+
+/*progressbar*/
+#progressbar {
+	margin-bottom: 30px;
+	overflow: hidden;
+	/*CSS counters to number the steps*/
+	counter-reset: step;
+}
+#progressbar li {
+	list-style-type: none;
+	color: black;
+	text-transform: uppercase;
+	font-size: 12px;
+	width: 33.33%;
+	float: left;
+	position: relative;
+}
+#progressbar li:before {
+	content: counter(step);
+	counter-increment: step;
+	width: 20px;
+	line-height: 20px;
+	display: block;
+	font-size: 12px;
+	color: #333;
+	background: white;
+	border-radius: 3px;
+	margin: 0 auto 5px auto;
+}
+/*progressbar connectors*/
+#progressbar li:after {
+	content: '';
+	width: 100%;
+	height: 2px;
+	background: white;
+	position: absolute;
+	left: -50%;
+	top: 9px;
+	z-index: -1; /*put it behind the numbers*/
+}
+#progressbar li:first-child:after {
+	/*connector not needed before the first step*/
+	content: none; 
+}
+/*marking active/completed steps green*/
+/*The number of the step and the connector before it = green*/
+#progressbar li.active:before,  #progressbar li.active:after{
+	background: #27AE60;
+	color: white;
+}
+
+p.error-text {
+	bottom: -23px;
+	left: 24px;
+	color: rgba(255, 0, 0, .7);
+	font-size: .8em;
+	margin-bottom: 0;
+}
+
+#term-error {
+	color: rgba(255, 0, 0, .7);
+	font-size: .8em;
+	bottom: -23px;
+	left: 24px;
+}
+
+select {
+	padding: 15px;
+	border: 1px solid #ccc;
+	border-radius: 3px;
+	margin-bottom: 10px;
+	width: 100%;
+	box-sizing: border-box;
+	font-family: montserrat;
+	color: #2C3E50;
+	font-size: 13px;
+
+	background-color: transparent;
+}
+
+.pl-0 {
+	padding-left: 0;
+}
+
+.terms {
+	font-size: .9rem;
+	width: 95%;
+	min-width: 250px;
+	height: auto;
+	overflow: scroll;
+	margin-bottom: 1rem;
+	margin-left: auto;
+	margin-right: auto;
+	border: 1px solid rgb(163, 163, 163);
+	line-height: 1.5rem;
+}
+
+.term-link a {
+	text-decoration: none;
+	color: black;
+}
+
+.h2 {
+	text-align: center;
+	font-size: 1.2rem;
+	font-weight: 500;
+	margin-top: 2rem;
+}
+
+.left_align {
+	font-size: 18px;
+	text-align: left;
+}
+
+#overlay {
+	position: fixed; /* Sit on top of the page content */
+	display: none;
+	width: 100%;
+	height: 100%;
+	top: 0; 
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0,0,0,0.5);
+	z-index: 2;
+	cursor: pointer;
+}
+
+.thankyou {
+	margin: auto;
+	min-width: 350px;
+	height: 250px;
+	margin-top: 190px;
+	background: #fff;
+	padding: 15px 20px;
+	line-height: 25px;
+	border-radius: 4px;
+	text-align: center;
+	
+}
+.thankyou input {
+	margin-top: 40px;
+}
+.thankyou h3 {
+	font-size: 2rem;
+	font-weight: 700;
+	color: #21ba45;
+	line-height: 2.5rem;
+	margin-bottom: 1.5rem;
+}
+
+.check_button {
+	display: none;
+}
+
+.fs-label-type {
+	background: transparent;
+	padding: 5px;
+	border: 1px solid black;
+	border-radius: 5px;
+	display: inline-block;
+}
+
+.check_button:checked + .fs-label-type{
+	background-color: #27AE60;
+	color: white;
+	padding: 5px;
+	border: 1px solid #27AE60;
+	border-radius: 5px;
+}
+
+#checker1, #checker2 {
+	display: inline;
+}
+
+.fs-label-info {
+	background: transparent;
+	padding: 5px;
+	border: 1px solid black;
+	border-radius: 5px;
+	display: inline-block;
+	margin-bottom: 8px;
+}
+
+input[type="radio"] {
+	display: none;
+}
+
+input[type="radio"]:checked + .fs-label-info {
+	background-color: #27AE60;
+	color: white;
+	padding: 5px;
+	border: 1px solid #27AE60;
+	border-radius: 5px;
+}
+
+input[type="checkbox"]:checked + .fs-label-info {
+	background-color: #27AE60;
+	color: white;
+	padding: 5px;
+	border: 1px solid #27AE60;
+	border-radius: 5px;
+}
+
+.btn-exit {
+	padding: .5rem .75rem;
+	background-color: transparent;
+	border: 1px solid black;
+	margin-top: .3rem;
+}
+
+.btn-term-exit {
+	padding: .5rem .75rem;
+	background-color: transparent;
+	border: 1px solid black;
+	margin-top: .3rem;
+	display: block;
+	margin-left: auto;
+	margin-right: auto;
+	margin-bottom: 2rem;
+}
+
+footer {
+	padding: 2rem;
+	padding-top: .5rem;
+}
+
+.footer {
+	display: flex;
+	justify-content: center;
+}
+
+.img_fr {
+	width: 80%;
+	max-width: 850px;
+	min-width: 300px;
+	height: 60px;
+	object-fit: contain;
+	object-position: center;
+}
+
+.img_fr img {
+	width: 100%;
+	height: 90%;
+}
+
+#msform #userid, #msform #area {
+	height: 0;
+	padding: 0;
+	margin: 0;
+	border: none;
+}
+
+#msform input[type="text"].error {
+	border-color: red;
+}
+
+.modal-header {
+	border-bottom: none;
+}
+
+#avatarmega .modal-content {
+	background-color: transparent;
+	border: none;
+}
+
+#avatarmega .modal-title {
+	color: white;
+}
+
+#avatarmega .btn-close {
+	background: none;
+}
+
+#avatarmega .modal-header {
+	position: absolute;
+	top: -.5rem;
+	right: 1.5rem;
+}
+
+.linker__box {
+	padding: .5rem 1rem;
+	border-radius: 1rem;
+	box-shadow: 1px 1px 5px 1px rgb(201, 201, 201);
+	width: max-content;
+	margin: 0 auto;
+	display: none;
+}
+
+.linker__box p {
+	margin-bottom: 0;
+	color: gray;
+}
+
+.linker__box i {
+	color: gray;
+	position: relative;
+}
+
+.linker__box i::after {
+	position: absolute;
+	content: " ";
+	right: -5px;
+	top: -10%;
+	border-right: 2px solid rgb(163, 163, 163);
+	width: 1px;
+	height: 120%;
+}
+
+.item_imgfr {
+	width: 15%;
+	height: 50px;
+	border-radius: 50%;
+	overflow: hidden;
+	margin: auto .5rem;
+}
+
+.item_imgfr img {
+	height: 100%;
+	width: 100%;
+}
+
+.content-box {
+	width: 75%;
+}
+
+.box-title {
+	margin-bottom: .2rem;
+	cursor: pointer;
+}
+
+.box-title:hover {
+	color: grey;
+	text-decoration: underline;
+
+}
+
+.box-link {
+	cursor: pointer;
+}
+
+.img_uploader {
+	display: none;
+}
+
+#msform input[type="text"].imgsrc {
+	width: calc(100% - 8rem);
+	margin-right: 5px;
+}
+
+.upload-btn {
+	display: inline-block;
+	width: 6rem;
+	background-color: #75a7dd;
+	color: white;
+	padding: .4rem .5rem;
+	font-size: .9rem;
+	border-radius: 3px;
+	transition: all .3s;
+	cursor: pointer;
+}
+
+.upload-btn img{
+	width: 30px;
+	height: 30px;
+}
+
+.upload-btn:hover {
+	background-color: #3b86d6;
+}
+
+.add, .addimg {
+	cursor: pointer;
+	display: inline-block;
+	width: 2rem;
+	height: 2rem;
+	background-color: #67abf3;
+	font-size: 1.2rem;
+	color: white;
+	border-radius: 50%;
+	transition: all .3s;
+}
+.add:hover, .addimg:hover {
+	background-color: #398ee9;
+}
+
+
+body {
+  font-family: "Lato", sans-serif;
+}
+.sidenav {
+  height: 100%;
+  width: 250px;
+  position: fixed;
+  z-index: 1;
+  top: 0;
+  left: 0;
+  background: linear-gradient(to bottom, #1C7CE0, #150051);
+  overflow-x: hidden;
+  transition: 0.5s;
+}
+
+.sidenav a {
+  text-decoration: none;
+  font-size: 25px;
+  color: #818181;
+  display: block;
+  transition: 0.3s;
+}
+
+.sidenav a:hover {
+  color: #f1f1f1;
+}
+
+.sidenav .closebtn {
+  position: absolute;
+  top: 0;
+  right: 25px;
+  font-size: 36px;
+  margin-left: 50px;
+}
+
+.container-login {
+	width: 100vw;
+    height: 100vh;
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    grid-gap :7rem;
+	padding: 0 2rem;
+}
+
+.navbar-nav button {
+	background: white;
+	border: none;
+	outline: none;
+}
+
+.img-wave {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	height: 100%;
+	z-index: -1;
+}
+
+.img-wave-profile {
+	height: 80%;
+}
+
+.row-img {
+	display: flex;
+	justify-content: flex-end;
+	align-items: center;
+}
+
+.row-login {
+	display: flex;
+	justify-content: flex-start;
+	align-items: center;
+	text-align: center;
+}
+
+.login-content {
+	width: 80%;
+	min-width: 350px;
+}
+
+.link_privacy {
+	color: black;
+}
+
+.login-content img {
+	height: 80px;
+}
+
+.row-img img {
+	width: 450px;
+}
+
+.login-content .form-floating input{
+	border: none;
+	border-bottom: 2px solid #d9d9d9;
+	border-radius: 0;
+}
+
+.login-content .form-floating input:focus{
+	box-shadow: none;
+}
+
+.login-content #btn_login {
+	background: linear-gradient(90deg, #0162c8, #55e7fc);
+	font-weight: bold;
+	color: white;
+	border: 0 none;
+	border-radius: 40px;
+	cursor: pointer;
+	padding: 10px 50px;
+	margin: 10px auto;
+	display: block;
+}
+
+.login-content #btn_login:hover {
+	box-shadow: 0 0 0 2px white, 0 0 0 3px #1C7CE0;
+}
+
+.login-content .nav-tabs {
+	border-bottom: none;
+}
+
+.login-content .nav {
+	flex-direction: column;
+	align-items: center;
+}
+
+.login-content .nav-tabs .nav-link {
+	color: grey;
+}
+
+.login-content .nav-tabs .nav-link.active {
+	border-color: #fff;
+	text-transform: uppercase;
+	color: black;
+}
+
+.navbar .btn-gocreate {
+	background: linear-gradient(90deg, #0162c8, #55e7fc);
+	color: white;
+	border: 0 none;
+	border-radius: 40px;
+	cursor: pointer;
+	padding: 10px 50px;
+}
+
+.container-profile {
+	width: 100vw;
+	height: 100vh;
+	overflow-y: hidden;
+}
+
+.container-bg {
+	position: absolute;
+	width: 600px;
+	height: 400px;
+	right: 0;
+	bottom: 0;
+}
+
+.container-profile .card-profile {
+	width: 25%;
+	min-width: 320px;
+	margin: auto;
+	margin-top: 4rem;
+	border-radius: 10px;
+	padding: 2rem;
+	text-align: center;
+	box-shadow: 0px 0px 20px 1px rgb(180, 180, 180);
+}
+
+.container-profile .card-profile img {
+	width: 120px;
+	margin: 1rem auto;
+}
+
+.card-profile-txt {
+	color: rgb(109, 109, 109);
+	font-size: 1.2rem;
+	font-weight: 500;
+}
+
+.card-profile-cnt {
+	font-size: 1.5rem;
+	font-weight: 700;
+}
+
+@media screen and (max-width: 900px){
+	.container-login{
+		grid-template-columns: 1fr;
+	}
+
+	.row-img{
+		display: none;
+	}
+
+	.img-wave {
+		display: none;
+	}
+
+	.row-login{
+		justify-content: center;
+	}
+	.container-bg {
+		width: 400px;
+		height: 300px;
+		right: 0;
+		bottom: 0;
+	}
+}
+
+@media screen and (max-height: 450px) {
+  .sidenav {padding-top: 15px;}
+  .sidenav a {font-size: 18px;}
+}

+ 107 - 0
api/templates/index.html

@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html lang="zh-TW">
+
+<head>
+    {% block head %}
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>AI Spokesgirl</title>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
+    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp"
+      crossorigin="anonymous">
+    <link rel="stylesheet"
+      href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt"
+      crossorigin="anonymous">
+      <link rel="preconnect" href="https://fonts.googleapis.com">
+      <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+      <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet"> 
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.min.css">
+    <link rel="stylesheet" href="static/owl.carousel.min.css">
+    <link rel="stylesheet" href="static/owl.theme.default.min.css">
+    <link rel="stylesheet" href="static/style.css">
+    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
+    <!-- <title>Login</title> -->
+    <title>{% block title %}{% endblock %} - My Webpage</title>
+    {% endblock %}
+</head>
+
+<body>
+    <!-- ================================================================= -->
+    <!-- navbar -->
+    <nav class="navbar navbar-expand-lg navbar-light">
+        <div class="container-fluid">
+            <a class="navbar-brand" href="/index">AI Spokesgirl</a>
+            <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
+                data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
+                aria-label="Toggle navigation">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+            <div class="collapse navbar-collapse" id="navbarSupportedContent">
+                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
+                    <li class="nav-item">
+                        <a class="nav-link active btn-gocreate text-white me-2" aria-current="page" href="/make_video" set-lan="html:make_video">製作影片</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link active btn-gocreate text-white" aria-current="page" href="/make_video_slide" set-lan="html:make_slides">SLIDE製作影片</a>                        
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link active btn-gocreate text-white" aria-current="page" href="/make_video_long" set-lan="html:make_slides">製作長影片</a>                        
+                    </li>
+                </ul>
+                
+                <ul class="navbar-nav mb-2 mb-lg-0">
+                    <li class="nav-item dropdown">
+                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+                            中/En
+                        </a>
+                        <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
+                            <li><button class="nav-link lan-swtich" aria-current="page" set-lan="html:en" onclick="changeLan(this)" value="en">English</button></li>
+                            <li><button class="nav-link lan-switch" aria-current="page" set-lan="html:zh" onclick="changeLan(this)" value="zh">中文</button></li>
+                        </ul>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link active" aria-current="page" href="/login" set-lan="html:login">登入</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" aria-current="page" href="/user_profile" set-lan="html:user_profile">會員資料</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" aria-current="page" href="/logout" set-lan="html:logout">登出</a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </nav>
+    <!-- ================================================================= -->
+
+
+    <!-- ================================================================= -->
+    <!-- content -->
+    <div class="container-fluid px-0">
+        {% block content %}{% endblock %}
+    </div>
+    <!-- ================================================================= -->
+
+
+    <!-- ================================================================= -->
+    <!-- footer -->
+    <footer class="fixed-bottom text-center py-2">
+        {% block footer %}
+        
+        {% endblock %}
+    </footer>
+    <!-- ================================================================= -->
+  
+    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
+    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.3/jquery.easing.min.js'></script>
+    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.min.js" integrity="sha384-Atwg2Pkwv9vp0ygtn1JAojH0nYbwNJLPhwyoVbhoPwBhjQPR5VtM2+xf0Uwh9KtT" crossorigin="anonymous"></script> 
+    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.all.min.js"></script>
+    <script src="static/owl.carousel.min.js"></script>
+    <script src="static/script_util.js"></script>
+    <script src="templates/script_index.js"></script>
+    <script type="text/javascript" src="static/lan.js"></script>
+</body>
+
+</html>

+ 92 - 0
api/templates/login.html

@@ -0,0 +1,92 @@
+{% extends "index.html" %}
+{% block title %}Login{% endblock %}
+{% block head %}
+{{ super() }}
+{% endblock %}
+{% block content %}
+
+
+<div class="container-login">
+    <img class="img-wave" src="static/img/wave.png" alt="">
+    <div class="row-img">
+        <img src="static/img/undraw_video_upload_3d4u.svg" alt="">
+    </div>
+    <div class="row-login">
+        <div class="login-content">
+            {% if success %}
+            <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
+                <symbol id="check-circle-fill" fill="currentColor" viewBox="0 0 16 16">
+                    <path
+                        d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
+                </symbol>
+            </svg>
+            <div class="alert alert-success d-flex align-items-center alert-dismissible" role="alert">
+                <svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Success:">
+                    <use xlink:href="#check-circle-fill" />
+                </svg>
+                <div>
+                    Registered successfully!
+                </div>
+                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+            </div>
+            {% endif %}
+            <img src="static/img/undraw_male_avatar_323b.svg" alt="">
+            <h2 class="my-3">WELCOME</h2>
+            <ul class="nav nav-tabs justify-content-center" id="myTab" role="tablist">
+                <li class="nav-item" role="presentation">
+                    <button class="nav-link active" id="login-tab" data-bs-toggle="tab" data-bs-target="#login"
+                        type="button" role="tab" aria-controls="login" aria-selected="true">Login</button>
+                </li>
+                <li class="nav-item" role="presentation">
+                    <button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button"
+                        role="tab" aria-controls="register" aria-selected="false">New Here? Register</button>
+                </li>
+            </ul>
+            <div class="tab-content" id="myTabContent">
+                <div class="tab-pane fade p-lg-3 show active" id="login" role="tabpanel" aria-labelledby="login-tab">
+                    
+                        <div class="form-floating mb-3">
+                            <input type="text" class="form-control" id="username" name="username" placeholder="User name">
+                            <label for="username"><i class="fas fa-user me-2"></i>User name</label>
+                        </div>
+                        <div class="form-floating mb-3">
+                            <input type="password" class="form-control" id="password" name="password" placeholder="Password">
+                            <label for="password"><i class="fas fa-lock me-2"></i>Password</label>
+                        </div>
+                        <div class="d-flex justify-content-center">
+                            <button id="btn_login" class="">Login</button>
+                        </div>
+            
+
+                </div>
+                <div class="tab-pane fade p-lg-3" id="register" role="tabpanel" aria-labelledby="register-tab">
+                    <form method="post" action="register">
+                        <div class="form-floating mb-3">
+                            <input type="text" class="form-control" id="username" name="username" placeholder="User name">
+                            <label for="username"><i class="fas fa-user me-2"></i>User name</label>
+                        </div>
+                        <div class="form-floating mb-3">
+                            <input type="email" class="form-control" id="email" name="email" placeholder="name@example.com">
+                            <label for="email"><i class="fas fa-envelope me-2"></i>Email address</label>
+                        </div>
+                        <div class="form-floating mb-3">
+                            <input type="password" class="form-control" id="password" name="password"  placeholder="Password">
+                            <label for="password"><i class="fas fa-lock me-2"></i>Password</label>
+                        </div>
+                        <div class="mb-3">
+                            <input type="checkbox" id="privacy" name="privacy" checked required>
+                            <label for="privacy"><a href="./privacy.html" class="link_privacy" target="_blank" set-lan="html:privacy_term">同意隱私政策及使用條款</a></label>
+                        </div>
+                        <div class="d-flex justify-content-center">
+                            <button type="submit" class="btn btn-primary align-items-center">Register</button>
+                        </div>
+
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+
+{% endblock %}

+ 170 - 0
api/templates/make_video.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>
+ <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="checker" 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 %}

+ 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>
+            <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" class="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 %}

+ 149 - 0
api/templates/make_video_slide.html

@@ -0,0 +1,149 @@
+{% extends "index.html" %}
+{% block title %}Login{% endblock %}
+{% block head %}
+{{ super() }}
+{% endblock %}
+{% block 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 Spokes Girl</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>使用說明</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>歷史紀錄</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>預覽影片</p>
+          <i class="fas fa-link"></i>
+          <a id='linker' style="display: none;" class="ms-2">影片連結</a>
+        </div>
+        <!-- fieldsets -->
+        <fieldset>
+          <h3 class="fs-subtitle">SLIDE 連結<img class="ms-1" src="static/img/question.png" alt="" data-bs-toggle="tooltip" data-bs-placement="right" title="將作為影片的內嵌標題"></h3>
+          <input id=slide_raw_url type="text" name='t1' class='title_new' value="" placeholder="連結" /><label for="myCheck">加入英文:</label> 
+          <input type="checkbox" id="multiLang" > <br/>
+        </fieldset>
+        <fieldset>
+          <h3  class="fs-subtitle">選擇人物<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 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>
+        <input id="send_slide" type="button" name="next" class="action-button" value="送出" />
+      </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">使用說明</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>1. 一句台詞請對應提供一個影像連結做為搭配</li>
+                      <li>2. 影像連結檔案格式支援:<stong class="strong">.png, jpg, .mp4</stong></li>
+                      <li>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">歷史紀錄</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 %}

+ 59 - 0
api/templates/privacy.html

@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html lang="en" >
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>隱私政策及使用條款</title>
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
+  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp"
+    crossorigin="anonymous">
+  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
+  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
+  <style>
+    .h2 {
+        text-align: center;
+        font-size: 1.2rem;
+        font-weight: 500;
+        margin-top: 2rem;
+    }
+    .terms {
+        font-size: .9rem;
+        width: 95%;
+        min-width: 250px;
+        height: auto;
+        overflow: scroll;
+        margin-bottom: 1rem;
+        margin-left: auto;
+        margin-right: auto;
+        border: 1px solid rgb(163, 163, 163);
+        line-height: 1.5rem;
+    }
+    .pl-0 {
+        padding-left: 0;
+    }
+  </style>
+</head>
+<body>
+    <div class="container">
+    
+    <main>
+        <h2 class="h2">隱私政策及使用條款</h2>
+        <div class="terms">
+          <ol class="pl-0">
+              <li>1. 集仕多股份有限公司(以下簡稱本公司),依需要取得您個人資料,並依個人資料保護法及相關法令規定,蒐集、處理及利用您個人資料。</li>
+          </ol>
+        </div>
+        
+        <!-- <button class="btn-term-exit" onclick="window.history.back();">回上一頁</button> -->
+    </main>
+    <div class="text-center">
+<!--
+      <img class="img-fluid mb-3" src="images/banner-bottom.jpg" alt="幸福空間 - 裝修市場問卷調查">
+-->
+
+    </div>
+  </div>
+    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
+    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.3/jquery.easing.min.js'></script>
+</body>
+</html>

+ 54 - 0
api/templates/script_index.js

@@ -0,0 +1,54 @@
+const btnLogin = document.querySelector('#btn_login');
+const inputPassword = document.querySelector('#password');
+
+inputPassword.addEventListener('keyup', loginByEnter);
+
+btnLogin.addEventListener('click', login);
+
+function loginByEnter(e) {
+  if (e.keyCode === 13) {
+    e.preventDefault();
+    console.log('login!');
+    login();
+  }
+};
+
+function login(){
+  console.log('login!');
+  var url = "login";
+  var xhr = new XMLHttpRequest();
+  xhr.open("POST", url);
+  xhr.setRequestHeader("accept", "application/json");
+  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+  xhr.onreadystatechange = function () {
+    if (xhr.readyState === 4) {
+      responseOBJ = JSON.parse(xhr.responseText)
+      // document.cookie = 'jwt_token='+responseOBJ.jwt_token    // access_token -> jwt_token
+      document.cookie = 'jwt_token='+responseOBJ.access_token;
+      if (responseOBJ.access_token!=null)
+      {
+        Swal.fire({
+          title: "登入成功",
+          icon: 'success',
+          confirmButtonColor: '#3085d6',
+        },function(isConfirm){
+          if(isConfirm){window.location.replace("/make_video");}
+        }
+        );
+      }
+      else{
+        Swal.fire({
+          title: "登入失敗",
+          icon: 'error',
+          text: responseOBJ.detail,
+          confirmButtonColor: '#3085d6',
+        });
+      }
+      
+    }
+  };
+  var data = "grant_type=&username=" + $('#username').val() + "&password="+$('#password').val()+"&scope=&client_id=&client_secret=";
+  result = xhr.send(data);
+  console.log(result);
+}
+

+ 22 - 0
api/templates/user_profile.html

@@ -0,0 +1,22 @@
+{% extends "index.html" %}
+{% block title %}Index{% endblock %}
+{% block head %}
+{{ super() }}
+{% endblock %}
+{% block content %} 
+
+<div class="container-profile">
+    <img class="img-wave img-wave-profile" src="static/img/wave.png" alt="">
+    <img class="container-bg" src="static/img/undraw_mobile_user_7oqo.svg" alt="">
+    <div class="card card-profile">
+        <img src="static/img/undraw_male_avatar_323b.svg" alt="">
+        <p class="card-profile-txt">User Profile</p>
+        <p class="card-profile-cnt">{{current_user}}</p>
+        <p class="card-profile-cnt">Email:</p>
+    </div>
+</div>
+
+{% endblock %} 
+
+   
+        

BIN
api/util/__pycache__/swap_face.cpython-39.pyc


BIN
api/util/__pycache__/user.cpython-39.pyc


+ 51 - 0
api/util/swap_face.py

@@ -0,0 +1,51 @@
+import os 
+import time
+import requests
+from PIL import Image
+import threading
+class swap_face():
+    def __init__(self, imgurl):
+        self.imgurl = imgurl
+    def run(self):
+        name_hash = str(time.time()).replace('.','')
+        src_img = 'FaceSwap/src_img/'+name_hash+'.jpg'
+        sv_path = '/var/www/html/swap_save/'+name_hash+'.avi'
+        print(name_hash)
+        
+        try:
+            im = Image.open(requests.get(self.imgurl, stream=True).raw)
+            im= im.convert("RGB")
+            im.save(src_img)
+        except:
+            return {'msg':'圖片錯誤'}
+        
+        x = threading.Thread(target=self.runthreadswap, args=(src_img,sv_path))
+        x.start()
+        time.sleep(20)
+        load_time = 0
+        while True:
+            print('waiting...')
+            if os.path.exists(sv_path):
+                break
+            time.sleep(10)
+            load_time+=10
+        
+            print('waiting...')
+        self.notify_group('人物生成成功,在:www.choozmo.com:8168/swap_save/'+name_hash+'.avi')
+        return {'msg':'生成中,成果會在line中展示'}
+
+    def runthreadswap(self,src_img,sv_path):
+        os.system('python3 FaceSwap/main_video.py --src_img '+src_img+' --video_path ./FaceSwap/b1.mp4 --correct_color --save_path '+sv_path)    
+        
+
+    def notify_group(self,msg):
+        headers = {
+                "Authorization": "Bearer " + "WekCRfnAirSiSxALiD6gcm0B56EejsoK89zFbIaiZQD",
+                "Content-Type": "application/x-www-form-urlencoded"
+        }
+        params = {"message": msg}   
+        r = requests.post("https://notify-api.line.me/api/notify",headers=headers, params=params)
+        #print(r)
+
+    
+        

+ 58 - 0
api/util/user.py

@@ -0,0 +1,58 @@
+class user_util():
+
+    def get_user_id(token):
+        db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+        credentials_exception = HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Could not validate credentials",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+        try:
+            payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+            username: str = payload.get("sub")
+            if username is None:
+                raise credentials_exception
+            token_data = models.TokenData(username=username)
+        except JWTError:
+            raise credentials_exception
+        user = get_user(username=token_data.username)
+        if user is None:
+            raise credentials_exception
+        user_id = first(db.query('SELECT * FROM users where username="' + user.username+'"'))['id']
+        return user_id
+
+    def check_user_exists(username):
+        db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+        if int(next(iter(db.query('SELECT COUNT(*) FROM AI_anchor.users WHERE username = "'+username+'"')))['COUNT(*)']) > 0:
+            return True
+        else:
+            return False
+
+    def get_user(username: str):
+        db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+        if not check_user_exists(username):  # if user don't exist
+            return False
+        user_dict = next(
+            iter(db.query('SELECT * FROM AI_anchor.users where username ="'+username+'"')))
+        user = models.User(**user_dict)
+        return user
+        
+    def user_register(user):
+        db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+        table = db['users']
+        user.password = get_password_hash(user.password)
+        table.insert(dict(user))
+
+    def get_password_hash(password):
+        return pwd_context.hash(password)
+    def verify_password(plain_password, hashed_password):
+        return pwd_context.verify(plain_password, hashed_password)
+    def authenticate_user(username: str, password: str):
+        db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+        if not check_user_exists(username):  # if user don't exist
+            return False
+        user_dict = next(iter(db.query('SELECT * FROM AI_anchor.users where username ="'+username+'"')))
+        user = models.User(**user_dict)
+        if not verify_password(password, user.password):
+            return False
+        return user

+ 26 - 0
etc/Dockerfile

@@ -0,0 +1,26 @@
+FROM ubuntu:20.04
+
+RUN apt update
+RUN apt install -y software-properties-common
+RUN add-apt-repository -y ppa:openshot.developers/ppa
+RUN apt update
+RUN apt-get update
+RUN DEBIAN_FRONTEND=noninteractive apt-get install keyboard-configuration
+RUN apt-get install python3-pip -y
+RUN apt-get install vim -y
+RUN pip3 install fastapi
+RUN pip3 install uvicorn[standard]
+RUN pip3 install numpy==1.19.2
+RUN pip3 install BeautifulSoup4
+RUN pip3 install Pillow
+RUN pip3 install pyttsx3
+RUN pip3 install zhtts
+RUN pip3 install rpyc
+RUN pip3 install websocket
+RUN pip3 install websocket-client
+RUN pip3 install dataset
+RUN pip3 install aiofiles
+EXPOSE 8022
+RUN apt-get install python3-openshot -y
+RUN apt-get install openshot-qt -y
+RUN apt install -y xvfb

+ 10 - 0
etc/docker cmd.txt

@@ -0,0 +1,10 @@
+sudo docker build -t openshot_service -< Dockerfile
+
+
+
+
+sudo docker run -d -it  --name tcontainer -p 8888:8888 -v /home/ming/workspace/AI_anchor_openshot:/app a8e05cc7c0c6
+
+
+
+uvicorn main:app --host="0.0.0.0" --reload --port 8888

+ 595 - 0
etc/main.py

@@ -0,0 +1,595 @@
+from fastapi import FastAPI,Cookie, Depends, FastAPI, Query, WebSocket, status, WebSocketDisconnect
+from os import listdir
+from os.path import isfile, isdir, join
+import threading
+import zhtts
+import os 
+import urllib
+from typing import List
+import requests
+from pydantic import BaseModel
+from bs4 import BeautifulSoup
+from PIL import Image,ImageDraw,ImageFont
+import pyttsx3
+import rpyc
+import random
+import time
+import math
+import hashlib
+import re
+import asyncio
+import urllib.request
+from fastapi.responses import FileResponse
+from websocket import create_connection
+from fastapi.middleware.cors import CORSMiddleware
+import dataset
+from datetime import datetime
+from util.swap_face import swap_face
+from fastapi.staticfiles import StaticFiles
+#service nginx restart 
+#uvicorn main:app --host="0.0.0.0" --reload --port 8878
+
+app = FastAPI()
+origins = [
+    "https://hhh.com.tw"
+    "http://172.105.205.52",
+    "http://172.105.205.52:8001",
+    "http://172.104.93.163",
+]
+
+app.add_middleware(
+    CORSMiddleware,
+    # allow_origins=origins,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+app.mount("/static", StaticFiles(directory="static"), name="static")
+app.mount("/static/img", StaticFiles(directory="static/img"), name="static/img")
+
+dir_sound = 'mp3_track/'
+dir_photo = 'photo/'
+dir_text = 'text_file/'
+dir_video = 'video_material/'
+dir_title = 'title/'
+dir_subtitle = 'subtitle/'
+dir_anchor = 'anchor_raw/'
+
+class swap_req(BaseModel):
+    imgurl: str
+
+class request(BaseModel):
+    name: str
+    text_content: List[str]
+    image_urls: List[str]
+    avatar: str
+    client_id :str
+
+
+class ConnectionManager:
+    def __init__(self):
+        self.active_connections: List[WebSocket] = []
+
+    async def connect(self, websocket: WebSocket):
+        await websocket.accept()
+        self.active_connections.append(websocket)
+
+    def disconnect(self, websocket: WebSocket):
+        self.active_connections.remove(websocket)
+
+    async def send_personal_message(self, message: str, websocket: WebSocket):
+        await websocket.send_text(message)
+
+    async def broadcast(self, message: str):
+        for connection in self.active_connections:
+            await connection.send_text(message)
+
+
+
+
+@app.get("/")
+async def root():
+    return {"message": "Hello, this is index"}
+
+@app.get("/index2")
+async def index2():
+    return FileResponse('static/index2.html')
+
+@app.get("/gen_avatar")
+async def index2():
+    return FileResponse('gen_avatar.html')
+
+@app.post("/swapFace")
+async def swapFace(req:swap_req):
+    sf = swap_face(req.imgurl)
+    result = sf.run()
+    #notify_group(result)hi
+    return result
+
+
+@app.post("/make_anchor_video_v2")
+async def make_anchor_video_v2(req:request):
+    for txt in req.text_content:
+        if re.search('[a-zA-Z]', txt) !=None:
+            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)
+                f=open(dir_photo+name_hash+"/"+str(img_num)+".mp4",'wb')
+            else:
+                im = Image.open(requests.get(imgu, stream=True).raw)
+                im= im.convert("RGB")
+        except:
+            return {'msg':"無法辨別圖片網址"+imgu}
+    
+    save_history(req,name_hash)
+    x = threading.Thread(target=anchor_video_v2, args=(name_hash,req.name, req.text_content, req.image_urls,int(req.avatar),req.client_id))
+    x.start()
+    return {"msg":"製作影片需要時間,請您耐心等候  稍後可以在www.choozmo.com:8168/"+name_hash+".mp4 中觀看"} 
+
+manager = ConnectionManager()
+@app.websocket("/progress/{client_id}")
+async def websocket_endpoint(websocket: WebSocket, client_id: int):
+    await manager.connect(websocket)
+    try:
+        while True:
+            data = await websocket.receive_text()
+            await manager.send_personal_message(data, websocket)
+            await manager.broadcast(data)
+    except WebSocketDisconnect:
+        manager.disconnect(websocket)
+        
+
+@app.get("/history_input")
+async def history_input():
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    statement = 'SELECT * FROM history_input ORDER BY timestamp DESC LIMIT 50'
+    logs = []
+    for row in db.query(statement):
+        logs.append({'id':row['id'],'name':row['name'],'text_content':row['text_content'].split(','),'link':row['link'],'image_urls':row['image_urls'].split(',')})
+    return logs
+
+def notify_group(msg):
+    glist=['7vilzohcyQMPLfAMRloUawiTV4vtusZhxv8Czo7AJX8','WekCRfnAirSiSxALiD6gcm0B56EejsoK89zFbIaiZQD','1dbtJHbWVbrooXmQqc4r8OyRWDryjD4TMJ6DiDsdgsX']
+    for gid in glist:
+        headers = {
+                "Authorization": "Bearer " + gid,
+                "Content-Type": "application/x-www-form-urlencoded"
+        }
+        params = {"message": msg}   
+        r = requests.post("https://notify-api.line.me/api/notify",headers=headers, params=params)
+
+
+def save_history(req,name_hash):
+    db = dataset.connect('mysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4')
+    log_table = db['history_input']
+    txt_content_seperate_by_dot = ''
+    for txt in req.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 req.image_urls:
+        img_urls_seperate_by_dot += iurl+","
+    img_urls_seperate_by_dot = img_urls_seperate_by_dot[:-1]
+    time_stamp = datetime.fromtimestamp(time.time())
+    time_stamp = time_stamp.strftime("%Y-%m-%d %H:%M:%S")
+    pk = log_table.insert({'name':req.name,'text_content':txt_content_seperate_by_dot,'image_urls':img_urls_seperate_by_dot,'link':'www.choozmo.com:8168/'+name_hash+'.mp4','timestamp':time_stamp})
+    
+
+def cKey(r,g,b,fuzz):
+    col=openshot.Color()
+    col.red=openshot.Keyframe(r)
+    col.green=openshot.Keyframe(g)
+    col.blue=openshot.Keyframe(b)
+    return openshot.ChromaKey(col, openshot.Keyframe(fuzz))
+
+def video_photo_clip(vid=None,layer=None, position=None, end=None
+    ,scale_x=1,scale_y=1,location_x=0,location_y=0,ck=None,audio=True):
+    clip = openshot.Clip(vid)
+    clip.Layer(layer)
+    clip.Position(position)
+    clip.End(end)
+    clip.scale_x=openshot.Keyframe(scale_x)
+    clip.scale_y=openshot.Keyframe(scale_y)
+    clip.location_x=openshot.Keyframe(location_x)
+    clip.location_y=openshot.Keyframe(location_y)
+    
+    if ck!=None:
+        clip.AddEffect(ck)
+    if audio==True:
+        clip.has_audio=openshot.Keyframe(1)
+    else:
+        clip.has_audio=openshot.Keyframe(0)
+    return clip
+
+
+
+
+def myunichchar(unicode_char):
+        mb_string = unicode_char.encode('big5')
+        try:
+            unicode_char = unichr(ord(mb_string[0]) << 8 | ord(mb_string[1]))
+        except NameError:
+            unicode_char = chr(mb_string[0] << 8 | mb_string[1])
+        return unicode_char
+
+
+def file_prepare(name, name_hash,text_content,image_urls):
+    #save image
+    try:
+        os.mkdir(dir_photo+name_hash)
+    except FileExistsError:
+        print("Directory " , dir_photo+name_hash ,  " already exists")
+    img_num = 1
+    for imgu in image_urls:
+        im = Image.open(requests.get(imgu, stream=True).raw)
+        im.save(dir_photo+name_hash+"/"+str(img_num)+".jpg")
+        img_num+=1
+    #save text
+    text_file = open(dir_text+name_hash+".txt", "w")
+    text_file.write(text_content)
+    text_file.close()
+    print("text file made")
+    #make mp3
+    tts = zhtts.TTS() 
+    tts.text2wav(text_content,dir_sound+name_hash+".mp3")
+    print("mp3 file made")
+    #make title as image
+    txt2image(name, dir_title+name_hash+".png")
+
+def get_url_type(url):
+    req = urllib.request.Request(url, method='HEAD', headers={'User-Agent': 'Mozilla/5.0'})
+    r = urllib.request.urlopen(req)
+    contentType = r.getheader('Content-Type')
+    return contentType
+    
+def downloadfile(name,url):
+    name=name+".mp4"
+    
+def make_dir(name_hash):
+    #save image
+    try:
+        os.mkdir(dir_photo+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_photo+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_text+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_text+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_sound+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_sound+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_video+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_video+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_anchor+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_anchor+name_hash ,  " already exists")
+    try:
+        os.mkdir(dir_subtitle+name_hash)
+    except FileExistsError:
+        print("~~~~~~Warning~~~~~~~~~Directory " , dir_subtitle+name_hash ,  " already exists")
+
+def file_prepare_v2(name, name_hash,text_content,image_urls):
+    make_dir(name_hash)
+    img_num = 1
+    for imgu in image_urls:
+        if get_url_type(imgu) =='video/mp4':
+            r=requests.get(imgu)
+            f=open(dir_photo+name_hash+"/"+str(img_num)+".mp4",'wb')
+            for chunk in r.iter_content(chunk_size=255): 
+                if chunk:
+                    f.write(chunk)
+            f.close()
+        else:
+            im = Image.open(requests.get(imgu, stream=True).raw)
+            im= im.convert("RGB")
+            im.save(dir_photo+name_hash+"/"+str(img_num)+".jpg")
+        img_num+=1
+    #save text
+    txt_idx=0
+    for txt in text_content:
+        text_file = open(dir_text+name_hash+"/"+str(txt_idx)+".txt", "w")
+        text_file.write(txt)
+        text_file.close()
+        txt_idx+=1
+    print("text file made")
+    #make mp3
+    language = 'zh-tw'
+    txt_idx = 0
+    for txt in text_content:
+        tts = zhtts.TTS() 
+        tts.text2wav(txt,dir_sound+name_hash+"/"+str(txt_idx)+".mp3")
+        txt_idx+=1
+    print("mp3 file made")
+    #make title as image
+    txt2image_title(name, dir_title+name_hash+".png")
+
+def txt2image(content, save_target):
+    unicode_text = trim_punctuation(content)
+    font = ImageFont.truetype(font="font/DFT_B7.ttc", size=38)
+    text_width, text_height = font.getsize(unicode_text)
+    canvas = Image.new('RGBA', (700, 500), (255, 0, 0, 0) )
+    draw = ImageDraw.Draw(canvas)
+    text= unicode_text
+    draw.text((5,5), text, (255, 255, 0), font)
+    canvas.save(save_target, "PNG")
+def txt2image_title(content, save_target):
+    unicode_text = trim_punctuation(content)
+    font = ImageFont.truetype(font="font/DFT_B7.ttc", size=28)
+    text_width, text_height = font.getsize(unicode_text)
+    canvas = Image.new('RGBA', (510, 500), (255, 0, 0, 0) )
+    draw = ImageDraw.Draw(canvas)
+    text= unicode_text
+    draw.text((5,5), text, (17, 41, 167), font)
+    canvas.save(save_target, "PNG")
+'''
+def txt2image_title(content, save_target):
+    unicode_text =content
+    font = ImageFont.truetype("font.ttf", 23,encoding='big5')
+    text_width, text_height = font.getsize(unicode_text)
+    canvas = Image.new('RGBA', (500, 500), (255, 0, 0, 0) )
+    draw = ImageDraw.Draw(canvas)
+    text=''
+    for c in unicode_text:
+        if len(re.findall(r'[\u4e00-\u9fff]+', c))>0:
+            text+=myunichchar(c)
+        else:
+            text+=c
+    draw.text((5,5), text, (17, 41, 167), font)
+    canvas.save(save_target, "PNG")
+'''
+def call_anchor(fileName,avatar):
+    conn = rpyc.classic.connect("192.168.1.105",18812)
+    ros = conn.modules.os 
+    rsys = conn.modules.sys 
+    fr=open(dir_sound+fileName+".mp3",'rb')# voice
+    #warning!!!    file my be replaced by other process
+    fw=conn.builtins.open('/tmp/output.mp3','wb')
+
+    while True:
+        b=fr.read(1024)
+        if b:
+            fw.write(b)
+        else:
+            break
+
+    fr.close()
+    fw.close()
+
+    val=random.randint(1000000,9999999)
+    ros.chdir('/home/jared/to_video')
+    ros.system('./p'+str(avatar)+'.sh '+str(val)+' &')
+
+    while True:
+        print('waiting...')
+        if ros.path.exists('/tmp/results/'+str(val)):
+            break
+        time.sleep(5)
+        print('waiting...')
+
+    fr=conn.builtins.open('/tmp/results/'+str(val)+'.mp4','rb')
+    fw=open(dir_anchor+fileName+".mp4",'wb')#peggy1_1
+    while True:
+        b=fr.read(1024)
+        if b:
+            fw.write(b)
+        else:
+            break
+
+    fr.close()
+    fw.close()
+
+
+
+def trim_punctuation(s):
+    pat_block = u'[^\u4e00-\u9fff0-9a-zA-Z]+';
+    pattern = u'([0-9]+{0}[0-9]+)|{0}'.format(pat_block)
+    res = re.sub(pattern, lambda x: x.group(1) if x.group(1) else u"" ,s)
+    return res
+
+def splitter(s):
+    for sent in re.findall(u'[^!?,。\!\?]+[!?。\!\?]?', s, flags=re.U):
+        yield sent
+
+def split_by_pun(s):
+    res = list(splitter(s))
+    return res
+
+def generate_subtitle_image(name_hash,text_content):
+    img_list = [None]*len(text_content)
+    for idx in range(len(text_content)):
+        img_list[idx]=[]
+        senList = split_by_pun(text_content[idx])
+        for inner_idx in range(len(senList)):
+            sv_path = dir_subtitle + name_hash +'/'+str(idx)+ str(inner_idx) +'.png'
+            sub = senList[inner_idx]
+            txt2image(sub,sv_path)
+            img_list[idx]+=[{"count":len(sub),"path":sv_path}]
+    return img_list
+
+async def sendProgress(progress,client_id):
+    ws = create_connection("ws://192.168.1.106:8887/progress/"+client_id)
+    ws.send(str(progress))
+    ws.close()
+
+def anchor_video_v2(name_hash,name,text_content, image_urls,avatar,client_id):
+    
+    progress = 0
+    asyncio.run(sendProgress(progress,client_id))
+    
+    
+    print('sub image made')
+    file_prepare_v2(name, name_hash, text_content,image_urls)
+    progress = 20
+    asyncio.run(sendProgress(progress,client_id))
+    sub_list=generate_subtitle_image(name_hash,text_content)
+    progress = 30
+    asyncio.run(sendProgress(progress,client_id))
+    
+    progress_per_video = int(40/len(text_content))
+    for fname in range(len(text_content)):
+        call_anchor(name_hash+"/"+str(fname),avatar)
+        progress += progress_per_video
+        print('step finish')
+        asyncio.run(sendProgress(progress,client_id))
+    print('called............................................')
+
+    ck=cKey(0,254,0,270)
+    ck_anchor=cKey(0,255,1,320)
+    duration = 0
+    #average layer level is 3
+    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.mp4")
+    LOGO_OP.Open()         # Open the reader
+    LOGO_OP_clip = video_photo_clip(vid=LOGO_OP,layer=4,position=0,end=LOGO_OP.info.duration
+                    ,location_y=-0.03,scale_x=0.8,scale_y=0.704)
+    t.AddClip(LOGO_OP_clip)
+    bg_head = openshot.FFmpegReader(dir_video+"bg_head.avi")
+    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 += LOGO_OP.info.duration
+    head_duration = LOGO_OP.info.duration
+    bg_head.Close()
+    LOGO_OP.Close()
+    progress += 10
+    
+
+    
+    clip_duration=0
+    photo_clip_list = [None]*len(text_content)
+    img_list = [None]*len(text_content)
+    anchor_clip_list = [None] * len(text_content)
+    anchor_list = [None] * len(text_content)
+    audio_clip_list = [None] * len(text_content)
+    audio_list = [None] * len(text_content)
+    sub_clip_list = [None] * len(text_content)
+    sub_img_list = [None] * len(text_content)
+    
+    idx = 0
+    for p in listdir(dir_photo+name_hash):
+        
+        anchor_list[idx] = openshot.FFmpegReader(dir_anchor+name_hash+"/"+str(idx)+".mp4")
+        clip_duration = anchor_list[idx].info.duration
+        anchor_list[idx].Open()
+        anchor_clip_list[idx] = video_photo_clip(vid=anchor_list[idx],layer=4,scale_x=0.65,scale_y=0.65,
+                location_x=0.35,location_y=0.25,position=main_timer, end=clip_duration,ck=ck_anchor,audio=False)
+        t.AddClip(anchor_clip_list[idx])
+
+        img_list[idx] = openshot.FFmpegReader(dir_photo+name_hash+'/'+p)
+        img_list[idx].Open()
+        photo_clip_list[idx] = video_photo_clip(vid=img_list[idx],layer=3
+                ,scale_x=0.81,scale_y=0.68,location_y=-0.03,position=main_timer,end=clip_duration,audio=False)
+        t.AddClip(photo_clip_list[idx])
+        img_list[idx].Close()
+
+        audio_list[idx] = openshot.FFmpegReader(dir_sound+name_hash+"/"+str(idx)+".mp3")
+        audio_list[idx].Open()
+        audio_clip_list[idx] = openshot.Clip(audio_list[idx])
+        audio_clip_list[idx].Position(main_timer)
+        audio_clip_list[idx].End(clip_duration)
+        t.AddClip(audio_clip_list[idx])
+
+        img_list[idx].Close()
+        anchor_list[idx].Close()
+        audio_list[idx].Close()
+
+     
+            
+        sub_img_list[idx] = [None] * len(sub_list[idx])
+        sub_clip_list[idx] = [None] * len(sub_list[idx])
+        sub_timer = 0
+        for sub_idx in range(len(sub_list[idx])):
+            sub_img_list[idx][sub_idx] = openshot.QtImageReader(sub_list[idx][sub_idx]['path'])
+            sub_img_list[idx][sub_idx].Open()
+            sub_duration = 0.205*sub_list[idx][sub_idx]['count']
+            sub_clip_list[idx][sub_idx] = video_photo_clip(vid=sub_img_list[idx][sub_idx], layer=6,location_x=0.069, location_y=0.89,position=main_timer+sub_timer,end=sub_duration)
+            t.AddClip(sub_clip_list[idx][sub_idx])
+            sub_img_list[idx][sub_idx].Close()
+            sub_timer += sub_duration
+            print(sub_list[idx][sub_idx]['path'])
+        main_timer += clip_duration
+        idx+=1
+
+    progress+=10
+    asyncio.run(sendProgress(progress,client_id))
+    
+    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+2
+                    ,location_x=0.005,location_y=-0.031
+                    ,scale_x=0.8,scale_y=0.6825)
+    t.AddClip(LOGO_ED_clip)
+    ED_duration = LOGO_ED.info.duration
+    LOGO_ED.Close()
+    
+
+    bg = openshot.FFmpegReader(dir_video+"bg.mp4")
+    bg.Open()
+    bg_times = math.floor(main_timer+ED_duration/bg.info.duration)
+    left_time = (main_timer+ED_duration) % 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+"bg.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+"bg.mp4")
+    bg_left.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)
+
+    ####start building
+    w = openshot.FFmpegWriter("../html/"+name_hash+".mp4")
+    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)
+    w.Open()
+    
+    #may change duration into t.info.duration
+    frames = int(t.info.fps)*int(head_duration+main_timer+ED_duration)
+    for n in range(frames):
+        f=t.GetFrame(n)
+        w.WriteFrame(f)
+        
+            
+    progress = 100
+    asyncio.run(sendProgress(progress,client_id))
+    notify_group(name+"的影片已經產生完成囉! www.choozmo.com:8168/"+name_hash+".mp4")
+    t.Close()
+    w.Close()
+
+
+    progress = 100
+    asyncio.run(sendProgress(progress,client_id))
+    print("Raw Video done")
+    print("video at : www.choozmo.com:8168/"+name_hash+".mp4")
+
+    #line notifs
+    
+
+

+ 14 - 0
subGenerator/.gitignore

@@ -0,0 +1,14 @@
+lib/
+python-libs/
+bin/
+*.spec
+*pyc
+*.egg-info
+*html
+build/
+tests/
+dist/
+.DS_Store
+MANIFEST
+*#*
+ffmpeg*

+ 96 - 0
subGenerator/ProcessSub.py

@@ -0,0 +1,96 @@
+from difflib import SequenceMatcher
+import os
+from gtts import gTTS
+from mutagen.mp3 import MP3
+import difflib
+#max_len = 3
+def similar(a, b):
+    return SequenceMatcher(None, a, b).ratio()
+
+def audio_compare_from_text(gt,gen):
+    tts=gTTS(text=gen, lang='zh')
+    tts.save("gen_tmp.mp3")
+    tts=gTTS(text=gt, lang='zh')
+    tts.save("gt_tmp.mp3")
+
+    audio = MP3("gen_tmp.mp3")
+    gen_len = audio.info.length
+    audio = MP3("gt_tmp.mp3")
+    gt_len = audio.info.length
+    
+    os.remove('gen_tmp.mp3')
+    os.remove('gt_tmp.mp3')
+    return gt_len, gen_len
+
+#break in nested loop only break 1 layer
+def adjustSub_by_audio_similarity(gt_array, generated_array):
+    for gen_idx in range(len(generated_array)):
+        #print('------------------------------------------------------------------------')
+        for gt_idx in range(len(gt_array)-max_len):
+            for l in range(max_len):
+                gt_text = gt_array[gt_idx]
+                gen_text = generated_array[gen_idx]
+                for idx_num in range(l):
+                    gt_text += gt_array[gt_idx+idx_num+1]
+                gt_len, gen_len = audio_compare_from_text(gt_text,gen_text)
+                #print(gt_text+'|'+str(gt_len)+'|'+gen_text+'|'+str(gen_len)+'|'+'SIMILARITY:'+str(similar(gt_text,gen_text)))
+                
+                if abs(gen_len - gt_len) < 0.3 and similar(gt_text,gen_text) > 0.5:
+                    generated_array[gen_idx] = gt_text 
+                    break
+    return generated_array
+'''
+gts = 'Hello Kitty 於2018年,加入YouTube開始活動,在自我介紹的影片裡,Kitty表示一直憧憬著,想在YouTube跟大家見面,一開頻道就吸引許多粉絲訂閱,目前有28萬訂閱者,接下來這位花生君,於2017年加入YouTube開始活動,他的外型太過特別,花生頭、紅色圍巾與紙尿布,被觀眾評價為,第一眼看上很噁心,但看著看著還挺可愛,目前有12萬訂閱者'.split(',')
+gens = ['Hello Kitty瑜2018年加入YouTube開始活動','再次我介紹的影片裡','Kitty表示一直憧憬著長在YouTube跟大家見面','一開頻道就吸引許多粉絲訂閱付錢有28萬訂閱者','接下來這位花生君瑜2017年加入YouTube開始湖','活動','他的外型太過特別花生桃紅色圍巾魚紙尿布','被觀眾評價為第一眼看上很噁心但看著看著還挺','秦可愛','目前有12萬訂閱者']
+adjs = adjustSub_by_gt_array(gts,gens)
+for s in adjs:
+    print(s)
+'''
+
+
+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:])]
+    alls = gts+combine2+combine3
+ 
+    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])
+            gens[idx] = match_text[0]
+        else:
+           print('無校正:'+gens[idx])
+adjustSub_by_text_similarity(gts,gens)

Some files were not shown because too many files changed in this diff