init project code
Browse files- app_runner.py +80 -0
- audio.py +37 -0
- environment.py +23 -0
- log_parser.py +51 -0
- main.py +55 -0
- page_runner.py +48 -0
- pages.py +82 -0
- report.py +207 -0
- requirement.txt +6 -0
- scripts/__init__.py +0 -0
- scripts/audio_utils.py +30 -0
- scripts/compare_text.py +64 -0
- scripts/demo_playwright.py +24 -0
- scripts/funasr_utils.py +101 -0
- scripts/run_llm.py +42 -0
- scripts/run_whisper.py +26 -0
- scripts/test_pages.py +35 -0
- tests/__init__.py +0 -0
- tests/conftest.py +56 -0
- tests/test_accuracy.py +37 -0
- tests/test_data/.gitattributes +1 -0
- tests/test_data/__init__.py +22 -0
- tests/test_data/test_audios/Chinese-calculus-part1.mp3 +3 -0
- tests/test_data/test_audios/Chinese-economics-part1.mp3 +3 -0
- tests/test_data/test_audios/Chinese-mayun-part2.mp3 +3 -0
- tests/test_data/test_audios/English-chaos-part2.wav +3 -0
- tests/test_data/test_audios/English-internet-part20.mp3 +3 -0
- tests/test_data/test_audios/English-legalsystem-part1.mp3 +3 -0
- tests/test_data/test_audios/English-literarytheory-part1.mp3 +3 -0
- tests/test_delay.py +34 -0
- tests/test_logfile.py +18 -0
- utils.py +65 -0
app_runner.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import signal
|
| 2 |
+
from typing import Literal
|
| 3 |
+
import subprocess
|
| 4 |
+
import time
|
| 5 |
+
import threading
|
| 6 |
+
import os
|
| 7 |
+
import signal
|
| 8 |
+
|
| 9 |
+
from environment import *
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class AppRunner:
|
| 13 |
+
def __init__(self, run_type: RunType):
|
| 14 |
+
self.run_type = run_type
|
| 15 |
+
self.sub_proc = None
|
| 16 |
+
|
| 17 |
+
def start(self):
|
| 18 |
+
print(f"start app by type: {self.run_type}")
|
| 19 |
+
if self.run_type==RunType.electron:
|
| 20 |
+
cmd_args = [APP_PATH, f"--remote-debugging-port={DEBUG_PORT}"]
|
| 21 |
+
cwd = None
|
| 22 |
+
log_file = APP_LOG
|
| 23 |
+
elif self.run_type == RunType.code:
|
| 24 |
+
cmd_args = ["python", str(CODE_PATH)]
|
| 25 |
+
cwd = CODE_DIR
|
| 26 |
+
log_file = CODE_LOG
|
| 27 |
+
else:
|
| 28 |
+
raise TypeError(f"invalid run_type: {self.run_type}")
|
| 29 |
+
self.clear_log(log_file)
|
| 30 |
+
self.sub_proc = subprocess.Popen(
|
| 31 |
+
cmd_args,
|
| 32 |
+
cwd=cwd,
|
| 33 |
+
stdout=subprocess.PIPE,
|
| 34 |
+
stderr=subprocess.PIPE,
|
| 35 |
+
preexec_fn= os.setsid
|
| 36 |
+
)
|
| 37 |
+
print(f"translator started, PID: {self.sub_proc.pid}")
|
| 38 |
+
time.sleep(10) # 等待app 启动
|
| 39 |
+
self.wait_for_app_start(log_file)
|
| 40 |
+
print(f"translator is ready.")
|
| 41 |
+
return self.sub_proc
|
| 42 |
+
def clear_log(self, log_path: Path):
|
| 43 |
+
if log_path.exists():
|
| 44 |
+
log_path.unlink(missing_ok=True)
|
| 45 |
+
|
| 46 |
+
def wait_for_app_start(self, log_file, timeout = 30, interval=3):
|
| 47 |
+
for i in range(int(timeout/interval)):
|
| 48 |
+
ret = subprocess.run(f"tail -n 10 {log_file}", check=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
| 49 |
+
universal_newlines=True)
|
| 50 |
+
if "Pipeline is ready" in ret.stdout:
|
| 51 |
+
print(ret.stdout)
|
| 52 |
+
return True
|
| 53 |
+
# print(ret.stdout)
|
| 54 |
+
print(f"app is not started yet, retry {i+1}...")
|
| 55 |
+
time.sleep(interval)
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
def stop(self):
|
| 59 |
+
pgid = os.getpgid(self.sub_proc.pid)
|
| 60 |
+
os.killpg(pgid, signal.SIGTERM)
|
| 61 |
+
try:
|
| 62 |
+
time.sleep(3)
|
| 63 |
+
return_code = self.sub_proc.wait(timeout=10)
|
| 64 |
+
print(f"进程组已终止,进程退出码: {return_code}")
|
| 65 |
+
except subprocess.TimeoutExpired:
|
| 66 |
+
print("超时,进程组可能未完全终止,尝试 SIGKILL...")
|
| 67 |
+
os.killpg(pgid, signal.SIGKILL)
|
| 68 |
+
self.sub_proc.wait(timeout=10)
|
| 69 |
+
print("进程组已被强制杀死。")
|
| 70 |
+
# stdout, stderr = self.sub_proc.communicate()
|
| 71 |
+
# print("\n最终 STDOUT:")
|
| 72 |
+
# print(stdout)
|
| 73 |
+
# print("\n最终 STDERR:")
|
| 74 |
+
# print(stderr)
|
| 75 |
+
|
| 76 |
+
if __name__ == '__main__':
|
| 77 |
+
app = AppRunner(RunType.code)
|
| 78 |
+
app.start()
|
| 79 |
+
print(app.wait_for_app_start())
|
| 80 |
+
app.stop()
|
audio.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pygame
|
| 2 |
+
import librosa
|
| 3 |
+
|
| 4 |
+
def play_audio(audio_path, volume=0.7):
|
| 5 |
+
pygame.mixer.init() # 初始化 pygame 的音频模块
|
| 6 |
+
try:
|
| 7 |
+
pygame.mixer.music.load(audio_path) # 加载音频文件
|
| 8 |
+
pygame.mixer.music.set_volume(volume)
|
| 9 |
+
pygame.mixer.music.play()
|
| 10 |
+
print(f"开始播放: {audio_path}")
|
| 11 |
+
except pygame.error as e:
|
| 12 |
+
print(f"播放音频文件时发生错误: {e}")
|
| 13 |
+
# finally:
|
| 14 |
+
# pygame.mixer.quit() # 退出 pygame 的音频模块
|
| 15 |
+
|
| 16 |
+
def play_audio_until_end(audio_path, volume=0.7):
|
| 17 |
+
pygame.mixer.init() # 初始化 pygame 的音频模块
|
| 18 |
+
try:
|
| 19 |
+
pygame.mixer.music.load(audio_path) # 加载音频文件
|
| 20 |
+
pygame.mixer.music.set_volume(volume)
|
| 21 |
+
pygame.mixer.music.play()
|
| 22 |
+
print(f"开始播放: {audio_path}")
|
| 23 |
+
while pygame.mixer.music.get_busy(): # 等待音频播放结束
|
| 24 |
+
pygame.time.Clock().tick(100) # 控制循环速度,避免 CPU 占用过高
|
| 25 |
+
print("音频播放结束")
|
| 26 |
+
except pygame.error as e:
|
| 27 |
+
print(f"播放音频文件时发生错误: {e}")
|
| 28 |
+
# finally:
|
| 29 |
+
# pygame.mixer.quit() # 退出 pygame 的音频模块
|
| 30 |
+
|
| 31 |
+
def get_length(audio_path):
|
| 32 |
+
try:
|
| 33 |
+
duration = librosa.get_duration(path=audio_path)
|
| 34 |
+
print(f"音频时长: {duration} 秒")
|
| 35 |
+
return duration
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"获取音频时长时发生错误: {e}")
|
environment.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from enum import Enum
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
PROJECT_DIR = Path("/Users/jeqin/work/code/TestTranslator")
|
| 7 |
+
APP_PATH = Path("/Applications/YoYo Translator.app/Contents/MacOS/YoYo Translator")
|
| 8 |
+
APP_LOG = Path('/tmp/translator.log')
|
| 9 |
+
CODE_DIR = Path("/Users/jeqin/work/code/Translator")
|
| 10 |
+
CODE_PATH = CODE_DIR / "main.py"
|
| 11 |
+
CODE_LOG = CODE_DIR / "translator.log"
|
| 12 |
+
|
| 13 |
+
DEBUG_PORT = 9222
|
| 14 |
+
TEST_DATA = PROJECT_DIR / "tests" / "test_data"
|
| 15 |
+
TEST_AUDIOS_DIR = TEST_DATA / "test_audios"
|
| 16 |
+
|
| 17 |
+
REPORTS_DIR = PROJECT_DIR / "reports"
|
| 18 |
+
SCREENSHOT_DIR = PROJECT_DIR / "screenshots"
|
| 19 |
+
|
| 20 |
+
class RunType(Enum):
|
| 21 |
+
code = 0
|
| 22 |
+
electron = 1
|
| 23 |
+
RUN_TYPE = RunType.electron # electron or web
|
log_parser.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from enum import Enum
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class LogTag(Enum):
|
| 8 |
+
load_start:str = "start initial TranslatePipe"
|
| 9 |
+
load_end:str = "Pipeline is ready."
|
| 10 |
+
audio_end:str = "Audio buffer length"
|
| 11 |
+
transcribe_end:str = "transcribe output"
|
| 12 |
+
transcribe_cost:str = "transcribe cost"
|
| 13 |
+
translate_start:str = "Translation input"
|
| 14 |
+
translate_end:str = "Translation out"
|
| 15 |
+
translate_cost:str = "Translate cost"
|
| 16 |
+
translate_large_end:str = "Translation large model output"
|
| 17 |
+
translate_large_cost:str = "Translate-large cost"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class LogItem:
|
| 22 |
+
timestamp: datetime
|
| 23 |
+
tag: LogTag
|
| 24 |
+
content: str = ""
|
| 25 |
+
|
| 26 |
+
@classmethod
|
| 27 |
+
def from_log(cls, log_tag, log_line):
|
| 28 |
+
try:
|
| 29 |
+
time_str = re.match("^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}", log_line).group(0)
|
| 30 |
+
timestamp = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S,%f")
|
| 31 |
+
# time_str = re.match("^\d{2}:\d{2}:\d{2}", log_line).group(0)
|
| 32 |
+
# timestamp = datetime.strptime(time_str, "%H:%M:%S")
|
| 33 |
+
res = re.match(".*?]:\s*(.*)", log_line)
|
| 34 |
+
content = ""
|
| 35 |
+
if res:
|
| 36 |
+
content = res.group(1)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(e)
|
| 39 |
+
print(log_line)
|
| 40 |
+
return cls(timestamp=timestamp,
|
| 41 |
+
tag=log_tag,
|
| 42 |
+
content=content)
|
| 43 |
+
@dataclass
|
| 44 |
+
class WebItem:
|
| 45 |
+
timestamp: datetime
|
| 46 |
+
src_text: str
|
| 47 |
+
dst_text: str
|
| 48 |
+
|
| 49 |
+
if __name__ == '__main__':
|
| 50 |
+
a = LogItem.from_log(LogTag.translate_finish, "2025-05-08 16:17:28,468 - INFO - [ 📝 transcribe output ]: Today is Friday.")
|
| 51 |
+
print(a)
|
main.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from report import LogReport
|
| 2 |
+
from audio import play_audio
|
| 3 |
+
import time
|
| 4 |
+
import os
|
| 5 |
+
import subprocess
|
| 6 |
+
from playwright.sync_api import sync_playwright
|
| 7 |
+
from utils import cmd
|
| 8 |
+
from environment import APP_PATH, DEBUG_PORT
|
| 9 |
+
from page_runner import PageRunner
|
| 10 |
+
from audio import play_audio_until_end
|
| 11 |
+
from tests.test_data import test_audios
|
| 12 |
+
|
| 13 |
+
if __name__ == '__main__':
|
| 14 |
+
# report = Report()
|
| 15 |
+
# report.from_logfile("/Users/jeqin/work/code/TestTranslator/logs/0508_zhen.log")
|
| 16 |
+
# report.to_csv("/Users/jeqin/work/code/TestTranslator/reports/test.csv")
|
| 17 |
+
# electron_app_path = '/Applications/YoYo Translator.app'
|
| 18 |
+
|
| 19 |
+
# electron_app_path = "/Applications/YoYo\ Translator.app/Contents/MacOS/YoYo\ Translator"
|
| 20 |
+
# pros = subprocess.Popen(
|
| 21 |
+
# [APP_PATH, f"--remote-debugging-port={DEBUG_PORT}"],
|
| 22 |
+
# stdout=subprocess.PIPE,
|
| 23 |
+
# stderr=subprocess.PIPE,
|
| 24 |
+
# text=True
|
| 25 |
+
# )
|
| 26 |
+
# time.sleep(15) # 等待页面加载
|
| 27 |
+
# print(f"{APP_PATH} started")
|
| 28 |
+
# # cmd(f"{electron_app_path} --remote-debugging-port=9222")
|
| 29 |
+
# # print("cmd finished")
|
| 30 |
+
# with sync_playwright() as p:
|
| 31 |
+
# # 连接到已开启的 Electron 调试端口
|
| 32 |
+
# browser = p.chromium.connect_over_cdp("http://localhost:9222")
|
| 33 |
+
# print(browser.is_connected())
|
| 34 |
+
# context = browser.contexts[0]
|
| 35 |
+
# page = context.pages[0]
|
| 36 |
+
# print(f"page title: {page.title()}")
|
| 37 |
+
# # page.screenshot(path="electron2.png")
|
| 38 |
+
# page.get_by_role("switch").click()
|
| 39 |
+
# print("clicked switch")
|
| 40 |
+
# print(f"page title: {page.title()}")
|
| 41 |
+
# time.sleep(2)
|
| 42 |
+
# page.screenshot(path="electron1.png")
|
| 43 |
+
#
|
| 44 |
+
# pros.terminate()
|
| 45 |
+
# stdout, stderr = pros.communicate()
|
| 46 |
+
# print("\n最终 STDOUT:")
|
| 47 |
+
# print(stdout)
|
| 48 |
+
# print("\n最终 STDERR:")
|
| 49 |
+
# print(stderr)
|
| 50 |
+
# p = PageRunner("electron").start()
|
| 51 |
+
# p.start_en2zh()
|
| 52 |
+
audios = test_audios.get("zh")
|
| 53 |
+
for a in audios:
|
| 54 |
+
play_audio_until_end(a)
|
| 55 |
+
time.sleep(5)
|
page_runner.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from typing import Literal
|
| 3 |
+
|
| 4 |
+
from playwright.sync_api import Playwright, sync_playwright
|
| 5 |
+
|
| 6 |
+
from pages import TranslatorPage
|
| 7 |
+
from environment import DEBUG_PORT, RunType
|
| 8 |
+
|
| 9 |
+
class PageRunner:
|
| 10 |
+
def __init__(self, run_type: RunType):
|
| 11 |
+
self.run_type = run_type
|
| 12 |
+
|
| 13 |
+
def start(self, p: Playwright)-> TranslatorPage:
|
| 14 |
+
if self.run_type == RunType.electron:
|
| 15 |
+
return TranslatorPage(self._start_electron(p))
|
| 16 |
+
elif self.run_type == RunType.code:
|
| 17 |
+
return TranslatorPage(self._start_web(p))
|
| 18 |
+
else:
|
| 19 |
+
raise TypeError(f"invalid run_type: {self.run_type}")
|
| 20 |
+
|
| 21 |
+
def _start_web(self, p: Playwright):
|
| 22 |
+
url = "http://127.0.0.1:9191/app"
|
| 23 |
+
browser = p.chromium.launch(headless=False)
|
| 24 |
+
context = browser.new_context(permissions=['microphone'])
|
| 25 |
+
page = context.new_page()
|
| 26 |
+
for i in range(20):
|
| 27 |
+
try:
|
| 28 |
+
page.goto(url)
|
| 29 |
+
break
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"Exception happened: {e}. retry {i+1}...")
|
| 32 |
+
time.sleep(3)
|
| 33 |
+
return page
|
| 34 |
+
|
| 35 |
+
def _start_electron(self, p: Playwright):
|
| 36 |
+
# 连接到已开启的 Electron 调试端口
|
| 37 |
+
browser = p.chromium.connect_over_cdp(f"http://localhost:{DEBUG_PORT}")
|
| 38 |
+
assert browser.is_connected()
|
| 39 |
+
print("connected to electron debugging port")
|
| 40 |
+
context = browser.contexts[0]
|
| 41 |
+
page = context.pages[0]
|
| 42 |
+
return page
|
| 43 |
+
|
| 44 |
+
if __name__ == '__main__':
|
| 45 |
+
with sync_playwright() as p:
|
| 46 |
+
page = PageRunner(RunType.code).start(p)
|
| 47 |
+
page.start_zh2en()
|
| 48 |
+
page.set_off()
|
pages.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from time import sleep
|
| 3 |
+
from typing import List, Literal
|
| 4 |
+
from playwright.sync_api import Page
|
| 5 |
+
|
| 6 |
+
from log_parser import WebItem
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class TranslatorPage:
|
| 10 |
+
def __init__(self, page:Page):
|
| 11 |
+
self.page = page
|
| 12 |
+
|
| 13 |
+
@property
|
| 14 |
+
def _switch(self):
|
| 15 |
+
return self.page.get_by_role("switch")
|
| 16 |
+
|
| 17 |
+
@property
|
| 18 |
+
def _trans_selector(self):
|
| 19 |
+
return self.page.locator(".ant-select-selector")
|
| 20 |
+
|
| 21 |
+
def start(self, translation_lang):
|
| 22 |
+
if translation_lang == "zh2en":
|
| 23 |
+
self.start_zh2en()
|
| 24 |
+
elif translation_lang == "en2zh":
|
| 25 |
+
self.start_en2zh()
|
| 26 |
+
else:
|
| 27 |
+
raise TypeError(f"invalid translation_lang: {translation_lang}")
|
| 28 |
+
|
| 29 |
+
def start_zh2en(self):
|
| 30 |
+
self._trans_selector.click()
|
| 31 |
+
self.page.locator(".ant-select-item").get_by_text("Chinese -> English").click()
|
| 32 |
+
self.set_on()
|
| 33 |
+
print("page started zh2en translation")
|
| 34 |
+
|
| 35 |
+
def start_en2zh(self):
|
| 36 |
+
self._trans_selector.click()
|
| 37 |
+
self.page.locator(".ant-select-item").get_by_text("English -> Chinese").click()
|
| 38 |
+
self.set_on()
|
| 39 |
+
print("page started en2zh translation")
|
| 40 |
+
|
| 41 |
+
def _is_on(self):
|
| 42 |
+
state = self._switch.get_attribute("aria-checked")
|
| 43 |
+
return True if state=="true" else False
|
| 44 |
+
|
| 45 |
+
def set_on(self):
|
| 46 |
+
if not self._is_on():
|
| 47 |
+
self._switch.click()
|
| 48 |
+
print("click button to set translation on")
|
| 49 |
+
|
| 50 |
+
def set_off(self):
|
| 51 |
+
if self._is_on():
|
| 52 |
+
self._switch.click()
|
| 53 |
+
print("click button to set translation off")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def get_current_node_text(self, duration=0, interval=0.1)-> List[WebItem]:
|
| 57 |
+
"""在一定时间内持续读取页面最新的 node的内容"""
|
| 58 |
+
print(f"capture page latest content for duration: {duration}s")
|
| 59 |
+
translate_items = []
|
| 60 |
+
last_src, last_dst = None, None
|
| 61 |
+
for i in range(int(duration // interval) + 1):
|
| 62 |
+
current_node = self.page.locator(".trans-list").locator(".current_node")
|
| 63 |
+
src_lang = current_node.locator(".trans-src-lang").inner_text()
|
| 64 |
+
dst_lang = current_node.locator(".trans-dst-lang").inner_text()
|
| 65 |
+
tsp = datetime.now()
|
| 66 |
+
|
| 67 |
+
sleep(interval)
|
| 68 |
+
if src_lang == last_src and dst_lang == last_dst:
|
| 69 |
+
continue
|
| 70 |
+
# print("src lang:", src_lang)
|
| 71 |
+
# print("dst lang:", dst_lang)
|
| 72 |
+
translate_items.append(WebItem(tsp, src_lang, dst_lang))
|
| 73 |
+
last_src, last_dst = src_lang, dst_lang
|
| 74 |
+
print(f"capture page latest content finished")
|
| 75 |
+
return translate_items
|
| 76 |
+
|
| 77 |
+
def get_translated_texts(self):
|
| 78 |
+
src_all = self.page.locator(".trans-list").locator(".trans-src-lang").all_inner_texts()
|
| 79 |
+
dst_all = self.page.locator(".trans-list").locator(".trans-dst-lang").all_inner_texts()
|
| 80 |
+
return "".join(src_all), "".join(dst_all)
|
| 81 |
+
|
| 82 |
+
|
report.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from dataclasses import dataclass, astuple
|
| 3 |
+
|
| 4 |
+
from tabulate import tabulate
|
| 5 |
+
from log_parser import LogTag, LogItem, WebItem
|
| 6 |
+
from utils import save_csv, run_textdistance, highlight_diff
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class Row:
|
| 10 |
+
audio_end_tsp:str = ""
|
| 11 |
+
audio_length:str =""
|
| 12 |
+
tsb_end_tsp:str =""
|
| 13 |
+
tsb_opt:str =""
|
| 14 |
+
tsb_cost:str =""
|
| 15 |
+
tsl_ipt:str =""
|
| 16 |
+
tsl_end_tsp:str =""
|
| 17 |
+
tsl_opt:str =""
|
| 18 |
+
tsl_cost:str =""
|
| 19 |
+
web_tsp:str =""
|
| 20 |
+
web_src:str =""
|
| 21 |
+
web_dst:str =""
|
| 22 |
+
def __repr__(self):
|
| 23 |
+
return f"Row(audio_length={self.audio_length}, tsb_opt={self.tsb_opt})"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class LogReport:
|
| 27 |
+
"""用于处理 log文件"""
|
| 28 |
+
def __init__(self):
|
| 29 |
+
self.items:List[LogItem] = []
|
| 30 |
+
|
| 31 |
+
def from_logfile(self, log_file, start_line=0):
|
| 32 |
+
"""将log文件中有效的行转换成 LogItem,返回 LogItem的列表
|
| 33 |
+
和当前文件的行数(用于下一个 case读取 log文件的起始行数)"""
|
| 34 |
+
print(f"generate LogReport from logfile: {log_file}")
|
| 35 |
+
with open(log_file, "r") as f:
|
| 36 |
+
lines = f.readlines()
|
| 37 |
+
print(f"read log file lines {start_line}:{len(lines)}")
|
| 38 |
+
for l in lines[start_line:]:
|
| 39 |
+
for item in LogTag:
|
| 40 |
+
if item.value in l:
|
| 41 |
+
log_item = LogItem.from_log(item, l)
|
| 42 |
+
self.items.append(log_item)
|
| 43 |
+
return self.items, len(lines)
|
| 44 |
+
|
| 45 |
+
def item_to_rows(self):
|
| 46 |
+
"""将 LogItem 列表转换成 csv行的格式,每行以 audio开始"""
|
| 47 |
+
rows = []
|
| 48 |
+
current_line = []
|
| 49 |
+
for index, item in enumerate(self.items):
|
| 50 |
+
if item.tag in [LogTag.load_start, LogTag.load_end]:
|
| 51 |
+
continue
|
| 52 |
+
# 每次检查到 audio_end就另起一行
|
| 53 |
+
if item.tag == LogTag.audio_end:
|
| 54 |
+
rows.append(current_line)
|
| 55 |
+
current_line = []
|
| 56 |
+
current_line += [item.tag.name, item.timestamp, item.content]
|
| 57 |
+
return rows
|
| 58 |
+
|
| 59 |
+
def to_csv(self, csv_path=None):
|
| 60 |
+
header_mapping = {
|
| 61 |
+
# 注释掉header,在 csv中就不保留对应的列
|
| 62 |
+
"audio_end_tag": 0,
|
| 63 |
+
"audio_end_tsp": 1,
|
| 64 |
+
"audio_length": 2,
|
| 65 |
+
"transcribe_cost_tag": 3,
|
| 66 |
+
"transcribe_cost_tsp": 4,
|
| 67 |
+
"transcribe_cost": 5,
|
| 68 |
+
"transcribe_end_tag": 6,
|
| 69 |
+
"transcribe_end_tsp": 7,
|
| 70 |
+
"transcribe_output": 8,
|
| 71 |
+
"translate_start_tag": 9,
|
| 72 |
+
"translate_start_tsp": 10,
|
| 73 |
+
"translate_input": 11,
|
| 74 |
+
"translate_cost_tag": 12,
|
| 75 |
+
"translate_cost_tsp": 13,
|
| 76 |
+
"translate_cost": 14,
|
| 77 |
+
"translate_end_tag": 15,
|
| 78 |
+
"translate_end_tsp": 16,
|
| 79 |
+
"translate_output": 17,
|
| 80 |
+
}
|
| 81 |
+
rows = self.item_to_rows()
|
| 82 |
+
header = list(header_mapping.keys())
|
| 83 |
+
rows = [[row[i] for i in header_mapping.values() if i < len(row)] for row in rows]
|
| 84 |
+
save_csv(csv_path, header, rows)
|
| 85 |
+
|
| 86 |
+
@dataclass
|
| 87 |
+
class DelayItem:
|
| 88 |
+
"""存储delay 报告中每一个 case的结果"""
|
| 89 |
+
translation_type: str = ''
|
| 90 |
+
audio: str = ""
|
| 91 |
+
audio_length: str = ""
|
| 92 |
+
web_items: List[WebItem] = None
|
| 93 |
+
log_items: List[LogItem] = None
|
| 94 |
+
|
| 95 |
+
def to_rows(self):
|
| 96 |
+
"""将 log和 web的结果合并成 csv行的形式
|
| 97 |
+
返回 row_0包含音频信息和 load 时间
|
| 98 |
+
rows 是每次推理的详细信息"""
|
| 99 |
+
print(f"length of log_items: {len(self.log_items)}")
|
| 100 |
+
web_items_dict = {i.src_text + i.dst_text: i for i in self.web_items}
|
| 101 |
+
|
| 102 |
+
row_0 = [self.translation_type, self.audio, self.audio_length]
|
| 103 |
+
rows = []
|
| 104 |
+
current_row = Row()
|
| 105 |
+
for i in self.log_items:
|
| 106 |
+
if i.tag in [LogTag.load_start, LogTag.load_end]:
|
| 107 |
+
row_0 += [i.tag.name, i.timestamp]
|
| 108 |
+
elif i.tag == LogTag.audio_end:
|
| 109 |
+
# 每次到 audio_end就是新的一行
|
| 110 |
+
rows.append(current_row)
|
| 111 |
+
current_row = Row()
|
| 112 |
+
current_row.audio_end_tsp = i.timestamp
|
| 113 |
+
current_row.audio_length = i.content.replace(" s", "")
|
| 114 |
+
elif i.tag == LogTag.transcribe_end:
|
| 115 |
+
current_row.tsb_end_tsp = i.timestamp
|
| 116 |
+
current_row.tsb_opt = i.content
|
| 117 |
+
elif i.tag == LogTag.transcribe_cost:
|
| 118 |
+
current_row.tsb_cost = i.content.replace(" s", "")
|
| 119 |
+
elif i.tag == LogTag.translate_start:
|
| 120 |
+
current_row.tsl_ipt = i.content
|
| 121 |
+
elif i.tag in [LogTag.translate_end, LogTag.translate_large_end]:
|
| 122 |
+
current_row.tsl_end_tsp = i.timestamp
|
| 123 |
+
current_row.tsl_opt = i.content
|
| 124 |
+
# 假设一行有翻译结果时,就一定已经有asr的结果
|
| 125 |
+
if web_item:=web_items_dict.get(current_row.tsb_opt+current_row.tsl_opt):
|
| 126 |
+
current_row.web_tsp = web_item.timestamp
|
| 127 |
+
current_row.web_src = web_item.src_text
|
| 128 |
+
current_row.web_dst = web_item.dst_text
|
| 129 |
+
# 删除 dict已匹配过的内容,避免多次匹配
|
| 130 |
+
web_items_dict.pop(current_row.tsb_opt+current_row.tsl_opt)
|
| 131 |
+
elif i.tag in [LogTag.translate_cost, LogTag.translate_large_cost]:
|
| 132 |
+
current_row.tsl_cost = i.content.replace(" s", "")
|
| 133 |
+
# print("rows value in DelayItem:",rows)
|
| 134 |
+
|
| 135 |
+
return row_0, rows # [astuple(i) for i in rows]
|
| 136 |
+
|
| 137 |
+
class DelayReport:
|
| 138 |
+
"""存储delay 报告中所有 case的结果"""
|
| 139 |
+
start_line = 0
|
| 140 |
+
items: List[DelayItem] = []
|
| 141 |
+
# summary_items = {
|
| 142 |
+
# "translation_type": "",
|
| 143 |
+
# "audio length": "",
|
| 144 |
+
# "load_model": "",
|
| 145 |
+
# "total_transcribe": "",
|
| 146 |
+
# "average_transcribe": "",
|
| 147 |
+
# "total_translate": "",
|
| 148 |
+
# "average_translate": "",
|
| 149 |
+
# "asr accuracy": "",
|
| 150 |
+
# "llm translation score": "",
|
| 151 |
+
# "delay": "",
|
| 152 |
+
# }
|
| 153 |
+
def print_summary(self, data):
|
| 154 |
+
header = ["audio", "load", "total audio len","total tsb","total tsl"]
|
| 155 |
+
print(tabulate(data, header))
|
| 156 |
+
|
| 157 |
+
def to_csv(self, csv_path):
|
| 158 |
+
all_rows = []
|
| 159 |
+
summaries = []
|
| 160 |
+
for i in self.items:
|
| 161 |
+
row_0, rows = i.to_rows()
|
| 162 |
+
all_rows.append(row_0)
|
| 163 |
+
all_rows += [astuple(i) for i in rows]
|
| 164 |
+
all_rows += [] # 每个 case后加一个空行
|
| 165 |
+
|
| 166 |
+
audios = [float(r.audio_length) for r in rows if r.audio_length]
|
| 167 |
+
transcribes = [float(r.tsb_cost) for r in rows if r.tsb_cost]
|
| 168 |
+
translates = [float(r.tsl_cost) for r in rows if r.tsl_cost]
|
| 169 |
+
if len(row_0) >=7:
|
| 170 |
+
summaries.append([row_0[1], row_0[6]-row_0[4], sum(audios), sum(transcribes), sum(translates)])
|
| 171 |
+
else:
|
| 172 |
+
summaries.append([row_0[1], 0, sum(audios), sum(transcribes), sum(translates)])
|
| 173 |
+
save_csv(csv_path, [], all_rows)
|
| 174 |
+
self.print_summary(summaries)
|
| 175 |
+
|
| 176 |
+
@dataclass
|
| 177 |
+
class AccuracyItem:
|
| 178 |
+
"""存储accuracy 报告中每一个 case的结果"""
|
| 179 |
+
translation_type: str = ''
|
| 180 |
+
audio: str = ""
|
| 181 |
+
audio_length: str = ""
|
| 182 |
+
audio_text: str = ""
|
| 183 |
+
src_text: str = ""
|
| 184 |
+
dst_text: str = ""
|
| 185 |
+
asr_accuracy: tuple= (0,1)
|
| 186 |
+
text_compare: str = ""
|
| 187 |
+
def __post_init__(self):
|
| 188 |
+
self.asr_accuracy = run_textdistance(self.audio_text, self.src_text)
|
| 189 |
+
self.text_compare = highlight_diff(self.audio_text, self.src_text)
|
| 190 |
+
def to_list(self):
|
| 191 |
+
return [self.translation_type, self.audio, self.audio_length, self.src_text,
|
| 192 |
+
# self.dst_text,
|
| 193 |
+
self.asr_accuracy, self.text_compare]
|
| 194 |
+
|
| 195 |
+
class AccuracyReport:
|
| 196 |
+
items:List[AccuracyItem] = []
|
| 197 |
+
|
| 198 |
+
def print_summary(self):
|
| 199 |
+
header = ["audio", "distance", "normalized distance"]
|
| 200 |
+
rows = [[i.audio, i.asr_accuracy[0], i.asr_accuracy[1]] for i in self.items]
|
| 201 |
+
print(tabulate(rows, header))
|
| 202 |
+
|
| 203 |
+
def to_csv(self, csv_path):
|
| 204 |
+
save_csv(csv_path, [], [i.to_list() for i in self.items])
|
| 205 |
+
self.print_summary()
|
| 206 |
+
|
| 207 |
+
|
requirement.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pytest
|
| 2 |
+
textdistance
|
| 3 |
+
pytest-playwright
|
| 4 |
+
pygame
|
| 5 |
+
librosa
|
| 6 |
+
tabulate
|
scripts/__init__.py
ADDED
|
File without changes
|
scripts/audio_utils.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import subprocess
|
| 3 |
+
from subprocess import CompletedProcess
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def cmd(command: str, check=True, capture_output=False) -> CompletedProcess:
|
| 7 |
+
print(command)
|
| 8 |
+
if capture_output:
|
| 9 |
+
ret = subprocess.run(command, shell=True, check=check, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
| 10 |
+
universal_newlines=True)
|
| 11 |
+
else:
|
| 12 |
+
ret = subprocess.run(command, shell=True, check=check)
|
| 13 |
+
print(ret.stdout)
|
| 14 |
+
return ret
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
current = Path("/Users/moyoyo/code/tests/audios")
|
| 18 |
+
audios_5s = current/"5s"
|
| 19 |
+
audios_10s = current/"10s"
|
| 20 |
+
for f in current.glob("*.wav"):
|
| 21 |
+
file_name = f.name
|
| 22 |
+
print(file_name)
|
| 23 |
+
for i in [0, 5, 10, 15]:
|
| 24 |
+
new_name = f"{f.name.split('.')[0]}-{i}.wav"
|
| 25 |
+
command=f"ffmpeg -i {f} -ss 00:00:{str(i).zfill(2)} -t 00:00:05 {audios_5s/new_name}"
|
| 26 |
+
cmd(command)
|
| 27 |
+
for i in [0, 10, 20, 30]:
|
| 28 |
+
new_name = f"{f.name.split('.')[0]}-{i}.wav"
|
| 29 |
+
command = f"ffmpeg -i {f} -ss 00:00:{str(i).zfill(2)} -t 00:00:10 {audios_10s/new_name}"
|
| 30 |
+
cmd(command)
|
scripts/compare_text.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
import re
|
| 3 |
+
import textdistance
|
| 4 |
+
import difflib
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def replace_symbol(text):
|
| 8 |
+
symbol_pattern = "[,.,。!?\n]"
|
| 9 |
+
to = ""
|
| 10 |
+
return re.sub(symbol_pattern, to, text)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def run_textdistance(text1, text2):
|
| 14 |
+
# text1 = replace_symbol(text1)
|
| 15 |
+
# text2 = replace_symbol(text2)
|
| 16 |
+
d = textdistance.levenshtein.distance(text1, text2)
|
| 17 |
+
nd = d / len(text1)
|
| 18 |
+
# print("Levenshtein distance of texts:", d, "normalized distance is:", nd)
|
| 19 |
+
return d, nd
|
| 20 |
+
|
| 21 |
+
def highlight_diff(a, b):
|
| 22 |
+
matcher = difflib.SequenceMatcher(None, a, b)
|
| 23 |
+
output = []
|
| 24 |
+
for tag, a_start, a_end, b_start, b_end in matcher.get_opcodes():
|
| 25 |
+
if tag == 'equal':
|
| 26 |
+
output.append(a[a_start:a_end])
|
| 27 |
+
elif tag == 'delete':
|
| 28 |
+
output.append(f"[-{a[a_start:a_end]}-]")
|
| 29 |
+
elif tag == 'insert':
|
| 30 |
+
output.append(f"{{+{b[b_start:b_end]}+}}")
|
| 31 |
+
elif tag == 'replace':
|
| 32 |
+
output.append(f"[-{a[a_start:a_end]}-]{{+{b[b_start:b_end]}+}}")
|
| 33 |
+
return ''.join(output)
|
| 34 |
+
|
| 35 |
+
def read_csv(file_path):
|
| 36 |
+
res ={}
|
| 37 |
+
with open(file_path, 'r', encoding='utf-8') as csvfile:
|
| 38 |
+
reader = csv.reader(csvfile)
|
| 39 |
+
for row in reader:
|
| 40 |
+
res[row[0]] = row[-1]
|
| 41 |
+
return res
|
| 42 |
+
|
| 43 |
+
def save_csv(file_path, rows):
|
| 44 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 45 |
+
writer = csv.writer(f)
|
| 46 |
+
writer.writerows(rows)
|
| 47 |
+
print(f"write csv to {file_path}")
|
| 48 |
+
|
| 49 |
+
def main():
|
| 50 |
+
funasr_text = read_csv("run_funasr.csv")
|
| 51 |
+
quant_text = read_csv("run_quant.csv")
|
| 52 |
+
print(funasr_text)
|
| 53 |
+
print(quant_text)
|
| 54 |
+
rows = [["file_name", "diff", "distance", "normalized_d"]]
|
| 55 |
+
for key, v in funasr_text.items():
|
| 56 |
+
d, normalized_d = run_textdistance(v, quant_text[key])
|
| 57 |
+
opt = highlight_diff(v, quant_text[key])
|
| 58 |
+
print(key,opt, d, normalized_d)
|
| 59 |
+
rows.append([key,opt, d, normalized_d])
|
| 60 |
+
save_csv("compare_asr.csv", rows)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == '__main__':
|
| 64 |
+
main()
|
scripts/demo_playwright.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from playwright.sync_api import Playwright, sync_playwright, expect
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def run(playwright: Playwright) -> None:
|
| 6 |
+
browser = playwright.chromium.launch(headless=False)
|
| 7 |
+
context = browser.new_context()
|
| 8 |
+
page = context.new_page()
|
| 9 |
+
page.goto("http://127.0.0.1:9191/app/")
|
| 10 |
+
page.get_by_text("English -> Chinese").click()
|
| 11 |
+
page.get_by_text("Chinese -> English").click()
|
| 12 |
+
page.locator("#app").get_by_title("Chinese -> English").click()
|
| 13 |
+
page.get_by_text("Chinese -> English").nth(1).click()
|
| 14 |
+
page.get_by_role("switch", name="ON OFF").click()
|
| 15 |
+
page.locator("#app").get_by_text("Chinese -> English").click()
|
| 16 |
+
page.get_by_text("Chinese -> English").nth(1).click()
|
| 17 |
+
|
| 18 |
+
# ---------------------
|
| 19 |
+
context.close()
|
| 20 |
+
browser.close()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
with sync_playwright() as playwright:
|
| 24 |
+
run(playwright)
|
scripts/funasr_utils.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import csv
|
| 3 |
+
import time
|
| 4 |
+
from funasr import AutoModel
|
| 5 |
+
from funasr_onnx import SeacoParaformer, CT_Transformer, Fsmn_vad
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
model_dir = Path("/Users/jeqin/work/code/Translator/moyoyo_asr_models")
|
| 9 |
+
|
| 10 |
+
def save_csv(file_path, rows):
|
| 11 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 12 |
+
writer = csv.writer(f)
|
| 13 |
+
writer.writerows(rows)
|
| 14 |
+
print(f"write csv to {file_path}")
|
| 15 |
+
|
| 16 |
+
def export_onnx():
|
| 17 |
+
asr_model_path = model_dir / 'speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch'
|
| 18 |
+
vad_model_path = model_dir / '/speech_fsmn_vad_zh-cn-16k-common-pytorch'
|
| 19 |
+
punc_model_path = model_dir / '/punc_ct-transformer_cn-en-common-vocab471067-large'
|
| 20 |
+
|
| 21 |
+
model = AutoModel(model=asr_model_path)
|
| 22 |
+
output = model.export(type="onnx", quantize=True, disable_update=True)
|
| 23 |
+
print(output)
|
| 24 |
+
|
| 25 |
+
model = AutoModel(model=vad_model_path)
|
| 26 |
+
output = model.export(type="onnx", quantize=True, disable_update=True)
|
| 27 |
+
print(output)
|
| 28 |
+
|
| 29 |
+
model = AutoModel(model=punc_model_path)
|
| 30 |
+
output = model.export(type="onnx", quantize=True, disable_update=True)
|
| 31 |
+
print(output)
|
| 32 |
+
|
| 33 |
+
def run_funasr():
|
| 34 |
+
asr_model_path = model_dir / 'speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch'
|
| 35 |
+
vad_model_path = model_dir / 'speech_fsmn_vad_zh-cn-16k-common-pytorch'
|
| 36 |
+
punc_model_path = model_dir / 'punc_ct-transformer_cn-en-common-vocab471067-large'
|
| 37 |
+
t0 = time.time()
|
| 38 |
+
model = AutoModel(
|
| 39 |
+
model=asr_model_path.as_posix(),
|
| 40 |
+
vad_model=vad_model_path.as_posix(),
|
| 41 |
+
punc_model=punc_model_path.as_posix(),
|
| 42 |
+
log_level="ERROR",
|
| 43 |
+
disable_update=True
|
| 44 |
+
)
|
| 45 |
+
t1 = time.time()
|
| 46 |
+
print("load model: ", t1 - t0)
|
| 47 |
+
audios = Path("/Users/jeqin/work/code/TestTranslator/tests/test_data/test_audios")
|
| 48 |
+
rows = [["file_name", "inference_time", "inference_result"]]
|
| 49 |
+
for audio in sorted(audios.glob("Chinese-mayun-part2.mp3")):
|
| 50 |
+
print(audio)
|
| 51 |
+
t1 = time.time()
|
| 52 |
+
try:
|
| 53 |
+
result = model.generate(input=str(audio), disable_pbar=True,
|
| 54 |
+
hotword="")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(audio)
|
| 57 |
+
print(e)
|
| 58 |
+
t2 = time.time()
|
| 59 |
+
t = t2 - t1
|
| 60 |
+
print("inference time:", t)
|
| 61 |
+
text = result[0]["text"]
|
| 62 |
+
print("inference result", text)
|
| 63 |
+
rows.append([f"{audio.parent.name}/{audio.name}", t, text])
|
| 64 |
+
save_csv(f"run_funasr.csv", rows)
|
| 65 |
+
|
| 66 |
+
def run_onnx(quant=True):
|
| 67 |
+
asr_model_path = model_dir / 'speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch'
|
| 68 |
+
vad_model_path = model_dir / 'speech_fsmn_vad_zh-cn-16k-common-pytorch'
|
| 69 |
+
punc_model_path = model_dir / 'punc_ct-transformer_cn-en-common-vocab471067-large'
|
| 70 |
+
t0 = time.time()
|
| 71 |
+
vad_model = Fsmn_vad(vad_model_path, quantize=quant)
|
| 72 |
+
asr_model = SeacoParaformer(asr_model_path, quantize=quant)
|
| 73 |
+
punc_model = CT_Transformer(punc_model_path, quantize=quant)
|
| 74 |
+
t1 = time.time()
|
| 75 |
+
print("load model: ", t1 - t0)
|
| 76 |
+
audios = Path("/Users/moyoyo/code/tests/audios")
|
| 77 |
+
rows = [["file_name", "inference_time", "inference_result"]]
|
| 78 |
+
for audio in sorted(audios.glob("*s/*.wav")):
|
| 79 |
+
t1 = time.time()
|
| 80 |
+
vad_res = vad_model(str(audio))
|
| 81 |
+
t2 = time.time()
|
| 82 |
+
# print("vad time:", t2 - t1)
|
| 83 |
+
asr_res = asr_model(str(audio), hotwords="")
|
| 84 |
+
asr_text = asr_res[0]["preds"]
|
| 85 |
+
t3 = time.time()
|
| 86 |
+
# print("asr time:", t3 - t2)
|
| 87 |
+
# print("asr text:", asr_text)
|
| 88 |
+
result = punc_model(asr_text)
|
| 89 |
+
text = result[0]
|
| 90 |
+
t4 = time.time()
|
| 91 |
+
# print("punc time:", t4 - t3)
|
| 92 |
+
# print("punc text:", text)
|
| 93 |
+
print(text)
|
| 94 |
+
t = t4 - t1
|
| 95 |
+
print("inference time:", t)
|
| 96 |
+
rows.append([f"{audio.parent.name}/{audio.name}", t, text])
|
| 97 |
+
file_name = "run_quant.csv" if quant else "run_onnx.csv"
|
| 98 |
+
save_csv(file_name, rows)
|
| 99 |
+
|
| 100 |
+
if __name__ == '__main__':
|
| 101 |
+
run_funasr()
|
scripts/run_llm.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from logging import getLogger
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from llama_cpp import Llama
|
| 4 |
+
from functools import lru_cache
|
| 5 |
+
|
| 6 |
+
logger = getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
class QwenTranslator:
|
| 9 |
+
def __init__(self, model_path, system_prompt_en="", system_prompt_zh="") -> None:
|
| 10 |
+
self.llm = Llama(
|
| 11 |
+
model_path=model_path,
|
| 12 |
+
chat_format="chatml",
|
| 13 |
+
verbose=False)
|
| 14 |
+
self.sys_prompt_en = system_prompt_en
|
| 15 |
+
self.sys_prompt_zh = system_prompt_zh
|
| 16 |
+
|
| 17 |
+
def to_message(self, prompt, src_lang, dst_lang):
|
| 18 |
+
"""构造提示词"""
|
| 19 |
+
return [
|
| 20 |
+
{"role": "system", "content": self.sys_prompt_en if src_lang == "en" else self.sys_prompt_zh},
|
| 21 |
+
{"role": "user", "content": prompt},
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
@lru_cache(maxsize=10)
|
| 25 |
+
def translate(self, prompt, src_lang, dst_lang) -> str:
|
| 26 |
+
message = self.to_message(prompt, src_lang, dst_lang)
|
| 27 |
+
output = self.llm.create_chat_completion(messages=message, temperature=0)
|
| 28 |
+
return output['choices'][0]['message']['content']
|
| 29 |
+
|
| 30 |
+
def __call__(self, prompt,*args, **kwargs):
|
| 31 |
+
return self.llm(
|
| 32 |
+
prompt,
|
| 33 |
+
*args,
|
| 34 |
+
**kwargs
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
if __name__ == '__main__':
|
| 38 |
+
model_dir = Path("/Users/jeqin/work/code/Translator/moyoyo_asr_models")
|
| 39 |
+
qwen2 = (model_dir / "qwen2.5-1.5b-instruct-q5_0.gguf").as_posix()
|
| 40 |
+
qwen3 = (model_dir / "Qwen_Qwen3-0.6B-Q4_K_M.gguf").as_posix()
|
| 41 |
+
|
| 42 |
+
# translator = QwenTranslator(qwen2, )
|
scripts/run_whisper.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pywhispercpp.model import Model
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from silero_vad.utils_vad import languages
|
| 6 |
+
|
| 7 |
+
models_dir = Path("/Users/jeqin/work/code/Translator/moyoyo_asr_models")
|
| 8 |
+
whisper_model = 'large-v3-turbo-q5_0'
|
| 9 |
+
model = Model(
|
| 10 |
+
model=whisper_model,
|
| 11 |
+
models_dir=models_dir,
|
| 12 |
+
print_realtime=False,
|
| 13 |
+
print_progress=False,
|
| 14 |
+
print_timestamps=False,
|
| 15 |
+
translate=False,
|
| 16 |
+
# beam_search=1,
|
| 17 |
+
temperature=0.,
|
| 18 |
+
no_context=True
|
| 19 |
+
)
|
| 20 |
+
audios = Path("/Users/jeqin/work/code/TestTranslator/tests/test_data/test_audios")
|
| 21 |
+
for audio in sorted(audios.glob("English*")):
|
| 22 |
+
print(audio)
|
| 23 |
+
t1 = time.time()
|
| 24 |
+
output = model.transcribe(str(audio), language="en")
|
| 25 |
+
print("inference time:", time.time()-t1)
|
| 26 |
+
print(" ".join([a.text for a in output]))
|
scripts/test_pages.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import time
|
| 3 |
+
from time import sleep
|
| 4 |
+
from playwright.sync_api import Page, expect
|
| 5 |
+
from audio import play_audio, get_length
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_has_title(page: Page, context):
|
| 9 |
+
context.grant_permissions(permissions=['microphone'])
|
| 10 |
+
page.goto("http://127.0.0.1:9191/")
|
| 11 |
+
page.get_by_text("English -> Chinese").click()
|
| 12 |
+
page.get_by_text("Chinese -> English").click()
|
| 13 |
+
page.get_by_role("switch").click()
|
| 14 |
+
|
| 15 |
+
audio = "/Users/jeqin/work/code/TestTranslator/test_audios/Chinese-economics-part1.wav"
|
| 16 |
+
play_audio(audio)
|
| 17 |
+
|
| 18 |
+
audio_length = get_length(audio)
|
| 19 |
+
interval = 0.1
|
| 20 |
+
last_src, last_dst = None, None
|
| 21 |
+
for i in range(int(audio_length//interval)+1):
|
| 22 |
+
print(i)
|
| 23 |
+
current_node = page.locator(".trans-list").locator(".current_node")
|
| 24 |
+
src_lang = current_node.locator(".trans-src-lang").inner_text()
|
| 25 |
+
dst_lang = current_node.locator(".trans-dst-lang").inner_text()
|
| 26 |
+
time.sleep(interval)
|
| 27 |
+
if src_lang == last_src and dst_lang == last_dst:
|
| 28 |
+
continue
|
| 29 |
+
print("src lang:", src_lang)
|
| 30 |
+
print("dst lang:", dst_lang)
|
| 31 |
+
last_src, last_dst = src_lang, dst_lang
|
| 32 |
+
|
| 33 |
+
all_texts = page.locator(".trans-list").locator(".trans-src-lang").all_inner_texts()
|
| 34 |
+
print("all_texts:", all_texts)
|
| 35 |
+
print("run_finished")
|
tests/__init__.py
ADDED
|
File without changes
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
from numpy.core.numeric import True_
|
| 5 |
+
from pytest import fixture
|
| 6 |
+
from playwright.sync_api import Page, expect
|
| 7 |
+
from playwright.sync_api import sync_playwright
|
| 8 |
+
|
| 9 |
+
from environment import *
|
| 10 |
+
from app_runner import AppRunner
|
| 11 |
+
from page_runner import PageRunner
|
| 12 |
+
from report import AccuracyReport, DelayReport, LogReport
|
| 13 |
+
from utils import get_time_str
|
| 14 |
+
|
| 15 |
+
@fixture(scope="session")
|
| 16 |
+
def log_file():
|
| 17 |
+
if RUN_TYPE == RunType.electron:
|
| 18 |
+
log_file = APP_LOG
|
| 19 |
+
elif RUN_TYPE == RunType.code:
|
| 20 |
+
log_file = CODE_LOG
|
| 21 |
+
else:
|
| 22 |
+
raise TypeError(f"invalid run_type: {RUN_TYPE}")
|
| 23 |
+
return log_file
|
| 24 |
+
|
| 25 |
+
@fixture(scope="session")
|
| 26 |
+
def app():
|
| 27 |
+
app = AppRunner(RUN_TYPE)
|
| 28 |
+
app.start()
|
| 29 |
+
yield app
|
| 30 |
+
app.stop()
|
| 31 |
+
|
| 32 |
+
@fixture(scope="module")
|
| 33 |
+
def page():
|
| 34 |
+
with sync_playwright() as p:
|
| 35 |
+
page = PageRunner(RUN_TYPE).start(p)
|
| 36 |
+
yield page
|
| 37 |
+
|
| 38 |
+
@fixture(scope="module")
|
| 39 |
+
def accuracy_report(request):
|
| 40 |
+
report = AccuracyReport()
|
| 41 |
+
yield report
|
| 42 |
+
report.to_csv(REPORTS_DIR/f"accuracy_report_{get_time_str()}.csv")
|
| 43 |
+
|
| 44 |
+
@fixture(scope="module")
|
| 45 |
+
def delay_report(request):
|
| 46 |
+
report = DelayReport()
|
| 47 |
+
yield report
|
| 48 |
+
report.to_csv(REPORTS_DIR/f"delay_report_{get_time_str()}.csv")
|
| 49 |
+
|
| 50 |
+
@fixture(scope="session")
|
| 51 |
+
def log_report(request):
|
| 52 |
+
report = LogReport()
|
| 53 |
+
yield report
|
| 54 |
+
report.to_csv(REPORTS_DIR/f"log_report_{get_time_str()}.csv")
|
| 55 |
+
|
| 56 |
+
|
tests/test_accuracy.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from audio import play_audio_until_end, get_length
|
| 6 |
+
from pages import TranslatorPage
|
| 7 |
+
from tests.test_data import test_audios, audio_texts
|
| 8 |
+
from report import AccuracyReport, AccuracyItem
|
| 9 |
+
|
| 10 |
+
@pytest.mark.parametrize("audio", test_audios.get("zh"))
|
| 11 |
+
def test_accuracy_zh2en(app, page: TranslatorPage, accuracy_report: AccuracyReport, audio:Path):
|
| 12 |
+
page.start_zh2en()
|
| 13 |
+
play_audio_until_end(audio)
|
| 14 |
+
time.sleep(4)
|
| 15 |
+
page.set_off()
|
| 16 |
+
zh, en = page.get_translated_texts()
|
| 17 |
+
accuracy_report.items.append(
|
| 18 |
+
AccuracyItem(translation_type="zh2en", audio=audio.name,
|
| 19 |
+
audio_length=get_length(audio), audio_text=audio_texts.get(audio.stem),
|
| 20 |
+
src_text=zh, dst_text=en)
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@pytest.mark.parametrize("audio", test_audios.get("en"))
|
| 25 |
+
def test_accuracy_en2zh(app, page: TranslatorPage, accuracy_report: AccuracyReport, audio):
|
| 26 |
+
page.start_en2zh()
|
| 27 |
+
play_audio_until_end(audio)
|
| 28 |
+
time.sleep(4)
|
| 29 |
+
page.set_off()
|
| 30 |
+
en, zh = page.get_translated_texts()
|
| 31 |
+
accuracy_report.items.append(
|
| 32 |
+
AccuracyItem(translation_type="en2zh", audio=audio.name,
|
| 33 |
+
audio_length=get_length(audio), audio_text=audio_texts.get(audio.stem),
|
| 34 |
+
src_text=en, dst_text=zh)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
tests/test_data/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
tests/test_data/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from environment import TEST_AUDIOS_DIR
|
| 2 |
+
|
| 3 |
+
test_audios = {
|
| 4 |
+
"zh": [
|
| 5 |
+
TEST_AUDIOS_DIR/"Chinese-calculus-part1.mp3",
|
| 6 |
+
TEST_AUDIOS_DIR/"Chinese-economics-part1.wav",
|
| 7 |
+
# TEST_AUDIOS_DIR / "Chinese-mayun-part2.mp3"
|
| 8 |
+
|
| 9 |
+
],
|
| 10 |
+
"en": [
|
| 11 |
+
TEST_AUDIOS_DIR/"English-chaos-part2.wav",
|
| 12 |
+
TEST_AUDIOS_DIR/"English-internet-part20.mp3",
|
| 13 |
+
]
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
audio_texts = {
|
| 17 |
+
"Chinese-calculus-part1": "后来我自己总结啊,微积分这么难,入门主要有几个原因。首先呢大部分的教材为了追求严谨,从一开始就使用了现代数学的这个所谓极限的概念。在它的基础之上向你介绍微积分。呃,问题是它是一个非常抽象的概念。对于大部分在接触微积分之前啊,主要的学习经验就是刷题啊,甚至是连题也不刷的。同学们来说呢,这种抽象语言会很陌生。而且如果你去了解微积分的历史的时候,你会发现极限这个概念啊是微积分创立之后大概一两百年才出现的这么个东西。你等于说我们现在公认的这些微积分的创始人,这些大佬们、牛顿啊、莱布尼茨、欧拉啊,连他们都不知道极限是什么。但是人家就是凭着直觉创建了微积分,当然作为教科书嘛,追求严谨无可厚非啊。虽然对于我来讲,过早的追求这种严谨,导致学习的人入门困难甚至入不了门。",
|
| 18 |
+
"Chinese-economics-part1": "经济就像一部简单的机器那样运行,但很多人不懂得这一点,或是对经济的运行方式持有不同观点,于是导致很多不必要的经济损失。我深感有责任与大家分享我的简单,但是实用的经济分析模式。这个模式虽然不符合常规传统经济学,但是已经帮助我预测和躲避了全球金融危机。三十多年来对我一直很有用。我们开始吧,经济虽然可能看起来复杂,但是其实是以简单和机械的方式运行。经济由几个简单的零部件和无数次重复的简单交易组成。这些交易首先是由人的天性所驱动,因而形成三股主要的经济动力,一、生产率的提高。二短期债务周期。三、长期债务周期。",
|
| 19 |
+
"Chinese-mayun-part2":"第二个公司我们成立了中国黄页。在中国黄业的创业经验中有很多的经验,也是可以要跟他这儿跟大家进行分享。九五年做互联网是最艰难的时候,就中国那时候还没联通互联网,我就到美国去了一趟。回来以后我们要做互联网,我请了二十四个朋友在我们家开会,说了两个小时,没人听懂。我在说什么。最后二十三个人反对一个人同意这一个人就说马云你这样做,你就试试看,不行的话,赶紧逃回来,还来得及。那我自己想了一个晚上,第二天早上我决定还是做下去。中国人很多创业是晚上想想千条路,早上起来走原路,晚上想想是热血沸腾,真好,第二天早上骑个自行车又上班去了,对吧?这是我们很多创业者所碰上的问题。说那一天我觉得因为我看见过互联网,我觉得互联网会将来会好,但这些人没看见过,但是没看见过的机会,就是你怎么把它变成现实。",
|
| 20 |
+
"English-chaos-part2": "This sequence of events is an example of what is known as the Butterfly Effect, a manifestation of Chaos Theory. For many centuries, the world was explained through the laws of Isaac Newton in classical physics. According to these laws, if the current state of an object is known, its future behavior can be predicted with relative ease. Chaos Theory questions this deterministic vision. Not everything is predictable anymore, nor does it work like a quirk. Since the 1800s, mathematicians have raised the idea that not all phenomena could be predicted by Newtonian laws. But a meteorologist named Edward Lawrence made Chaos Theory a visible phenomenon. It all started in 1961, when he was working on a mathematical model to forecast the weather. Lawrence entered data such as temperature, humidity, pressure, and wind direction into his computer. His computer would draw a graph modeling what the weather would be like. Not always accurate, but very close to reality.",
|
| 21 |
+
"English-internet-part20": "Many, many years ago in the early 1970s, my partner Bob Kahn and I began working on the design of what we now call the Internet. Bob and I had the responsibility and the opportunity to design the Internet's protocols and its architecture. So we persisted in participating in the Internet's growth and evolution for all of this time up to and including the present. The way information gets transferred from one computer to another is pretty interesting. It need not follow a fixed path. In fact, your path may change in the midst of a computer-to-computer conversation. Information on the Internet goes from one computer to another in what we call a packet of information. And a packet travels from one place to another on the Internet a lot like how you might get from one place to another in a car. Depending on traffic congestion or road conditions, you might choose or be forced to take a different route to get to the same place each time you travel."
|
| 22 |
+
}
|
tests/test_data/test_audios/Chinese-calculus-part1.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:43b9658edd50a6a69bb509960d5ee90e33078a3f84e116eb586fd2d903f6009a
|
| 3 |
+
size 1041064
|
tests/test_data/test_audios/Chinese-economics-part1.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:60caa6584b37308ce048cc2bbbac1b48a37518a728683057fcfbe49aea0d0b44
|
| 3 |
+
size 977023
|
tests/test_data/test_audios/Chinese-mayun-part2.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:47ac644833dac81de0efa7d1784924a293f4b07a8008c821c37c1255c96ca158
|
| 3 |
+
size 992999
|
tests/test_data/test_audios/English-chaos-part2.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9548f6d69ca750eb965ee1e8b45fdf7f5c9060639c686c003f8f24ea78a23a09
|
| 3 |
+
size 4032078
|
tests/test_data/test_audios/English-internet-part20.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c481a27b50e5ce2487a9556becd22b5ab9bf33e136b59f0973f5e6cd5c1ae0c6
|
| 3 |
+
size 992906
|
tests/test_data/test_audios/English-legalsystem-part1.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cbca4d430e4451b4c2aebd70a21b6e19ba9670cf609d4e88f795e9862cb5268b
|
| 3 |
+
size 817038
|
tests/test_data/test_audios/English-literarytheory-part1.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:20af50212f5e60170c27973a29870cb6615d33e7de9465f4bd5b6239b5c23cb8
|
| 3 |
+
size 880986
|
tests/test_delay.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from audio import get_length, play_audio
|
| 5 |
+
from pages import TranslatorPage
|
| 6 |
+
from report import DelayReport, DelayItem, LogReport
|
| 7 |
+
from tests.test_data import test_audios
|
| 8 |
+
|
| 9 |
+
@pytest.mark.parametrize("audio", test_audios.get("zh"))
|
| 10 |
+
def test_delay_zh2en(log_file, app, delay_report: DelayReport,page: TranslatorPage, audio:Path):
|
| 11 |
+
page.start_zh2en()
|
| 12 |
+
audio_length = get_length(audio)
|
| 13 |
+
play_audio(audio)
|
| 14 |
+
web_records = page.get_current_node_text(duration=audio_length)
|
| 15 |
+
log_records, delay_report.start_line = LogReport().from_logfile(log_file, delay_report.start_line)
|
| 16 |
+
delay_report.items.append(
|
| 17 |
+
DelayItem(translation_type="zh2en", audio=audio.name, audio_length=audio_length,
|
| 18 |
+
web_items=web_records, log_items=log_records,)
|
| 19 |
+
)
|
| 20 |
+
page.set_off()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@pytest.mark.parametrize("audio", test_audios.get("en"))
|
| 24 |
+
def test_delay_en2zh(log_file,app, delay_report: DelayReport,page: TranslatorPage, audio:Path):
|
| 25 |
+
page.start_en2zh()
|
| 26 |
+
audio_length = get_length(audio)
|
| 27 |
+
play_audio(audio)
|
| 28 |
+
web_records = page.get_current_node_text(duration=audio_length)
|
| 29 |
+
log_records, delay_report.start_line = LogReport().from_logfile(log_file, delay_report.start_line)
|
| 30 |
+
delay_report.items.append(
|
| 31 |
+
DelayItem(translation_type="zh2en", audio=audio.name, audio_length=audio_length,
|
| 32 |
+
web_items=web_records, log_items=log_records, )
|
| 33 |
+
)
|
| 34 |
+
page.set_off()
|
tests/test_logfile.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from report import LogReport, DelayItem
|
| 5 |
+
from environment import REPORTS_DIR
|
| 6 |
+
from utils import get_time_str
|
| 7 |
+
|
| 8 |
+
test_files = [
|
| 9 |
+
# Path("/Users/jeqin/work/code/Translator/translator.log"),
|
| 10 |
+
Path("/Users/jeqin/Downloads/translator.log"),
|
| 11 |
+
# Path("/Users/jeqin/Downloads/translator-web-0527.log"),
|
| 12 |
+
# Path("/tmp/translator.log"),
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
@pytest.mark.parametrize("test_file", test_files)
|
| 16 |
+
def test_zh2en_logfile(test_file: Path, log_report):
|
| 17 |
+
log_report.from_logfile(test_file)
|
| 18 |
+
# log_report.to_csv(REPORTS_DIR/f"logfile_{get_time_str()}.csv")
|
utils.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
import subprocess
|
| 4 |
+
from subprocess import CompletedProcess
|
| 5 |
+
from typing import Literal
|
| 6 |
+
import re
|
| 7 |
+
import difflib
|
| 8 |
+
|
| 9 |
+
import textdistance
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_time_str(level:Literal["d","s","ms"]="d"):
|
| 13 |
+
time = datetime.now()
|
| 14 |
+
if level == "d":
|
| 15 |
+
return time.strftime("%Y-%m-%d")
|
| 16 |
+
if level == "s":
|
| 17 |
+
return time.strftime("%H%M%S")
|
| 18 |
+
if level == "ms":
|
| 19 |
+
return time.strftime("%H%M%S.%f")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def save_csv(file_path, header, rows):
|
| 23 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 24 |
+
writer = csv.writer(f)
|
| 25 |
+
writer.writerow(header)
|
| 26 |
+
writer.writerows(rows)
|
| 27 |
+
print(f"write csv to {file_path}")
|
| 28 |
+
|
| 29 |
+
def cmd(command: str, check=True, capture_output=False) -> CompletedProcess:
|
| 30 |
+
print(command)
|
| 31 |
+
if capture_output:
|
| 32 |
+
ret = subprocess.run(command, shell=True, check=check, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
| 33 |
+
universal_newlines=True)
|
| 34 |
+
else:
|
| 35 |
+
ret = subprocess.run(command, shell=True, check=check)
|
| 36 |
+
print(ret.stdout)
|
| 37 |
+
return ret
|
| 38 |
+
|
| 39 |
+
def replace_symbol(text):
|
| 40 |
+
symbol_pattern = "[,.,。!?\n]"
|
| 41 |
+
to = ""
|
| 42 |
+
return re.sub(symbol_pattern, to, text)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def run_textdistance(text1, text2):
|
| 46 |
+
text1 = replace_symbol(text1)
|
| 47 |
+
text2 = replace_symbol(text2)
|
| 48 |
+
d = textdistance.levenshtein.distance(text1, text2)
|
| 49 |
+
nd = d / len(text1)
|
| 50 |
+
# print("Levenshtein distance of texts:", d, "normalized distance is:", nd)
|
| 51 |
+
return d, nd
|
| 52 |
+
|
| 53 |
+
def highlight_diff(a, b):
|
| 54 |
+
matcher = difflib.SequenceMatcher(None, a, b)
|
| 55 |
+
output = []
|
| 56 |
+
for tag, a_start, a_end, b_start, b_end in matcher.get_opcodes():
|
| 57 |
+
if tag == 'equal':
|
| 58 |
+
output.append(a[a_start:a_end])
|
| 59 |
+
elif tag == 'delete':
|
| 60 |
+
output.append(f"[-{a[a_start:a_end]}-]")
|
| 61 |
+
elif tag == 'insert':
|
| 62 |
+
output.append(f"{{+{b[b_start:b_end]}+}}")
|
| 63 |
+
elif tag == 'replace':
|
| 64 |
+
output.append(f"[-{a[a_start:a_end]}-]{{+{b[b_start:b_end]}+}}")
|
| 65 |
+
return ''.join(output)
|