Spaces:
Sleeping
Sleeping
AKIRA
commited on
Commit
·
b3b0b53
1
Parent(s):
ad71343
Finalize all local changes
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .DS_Store +0 -0
- .gitignore +2 -0
- app.js +6 -0
- app.json +25 -0
- app.py +147 -113
- asr.py +31 -0
- asr_server.py +78 -0
- ecosystem.config.js +8 -0
- pages/index/.DS_Store +0 -0
- pages/index/index-original-全部依托于HF进行识别翻译的版本-可以运行.js +174 -0
- pages/index/index.js +42 -27
- pages/index/index.json +1 -0
- pages/index/index.wxml +49 -0
- pages/index/index.wxss +112 -0
- pages/index/index_v10_old.js +160 -0
- pages/index/index_v11_old.js +79 -0
- pages/index/index_v12_old.js +83 -0
- pages/index/index_v13_old.js +79 -0
- pages/index/index_v14_同声传译API_only-可以运行_old.js +223 -0
- pages/index/index_v15_old.js +192 -0
- pages/index/index_v16_old.js +192 -0
- pages/index/index_v17_old.js +222 -0
- pages/index/index_v18_old.js +231 -0
- pages/index/index_v19_old.js +234 -0
- pages/index/index_v20_old.js +179 -0
- pages/index/index_v21_old.js +177 -0
- pages/index/index_v22_old.js +197 -0
- pages/index/index_v24_old.js +197 -0
- pages/index/index_v25_old.js +203 -0
- pages/index/index_v26_old.js +150 -0
- pages/index/index_v27_old.js +159 -0
- pages/index/index_v28_old.js +139 -0
- pages/index/index_v2_old.js +232 -0
- pages/index/index_v3_old.js +173 -0
- pages/index/index_v4_old.js +169 -0
- pages/index/index_v5_old.js +162 -0
- pages/index/index_v6_old.js +150 -0
- pages/index/index_v7_old.js +157 -0
- pages/index/index_v8_old.js +153 -0
- pages/index/index_v9_old.js +75 -0
- pages/index/indexz_v23_old.js +180 -0
- project.config.json +68 -0
- project.private.config.json +22 -0
- requirements.txt +1 -2
- server/package-lock.json +917 -0
- server/package.json +19 -0
- server/server.js +97 -0
- server/server.js.old +64 -0
- start_asr.sh +30 -0
- translate.py +51 -0
.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
.gitignore
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
# Ignore local environment variables and secrets
|
2 |
+
.env
|
app.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// app.js
|
2 |
+
App({
|
3 |
+
onLaunch() {
|
4 |
+
// 小程序启动时执行的逻辑
|
5 |
+
}
|
6 |
+
})
|
app.json
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"pages": [
|
3 |
+
"pages/index/index"
|
4 |
+
],
|
5 |
+
"permission": {
|
6 |
+
"scope.record": {
|
7 |
+
"desc": "用于实时语音识别"
|
8 |
+
}
|
9 |
+
},
|
10 |
+
"window": {
|
11 |
+
"backgroundTextStyle": "light",
|
12 |
+
"navigationBarBackgroundColor": "#fff",
|
13 |
+
"navigationBarTitleText": "实时翻译",
|
14 |
+
"navigationBarTextStyle": "black"
|
15 |
+
},
|
16 |
+
"networkTimeout": {
|
17 |
+
"request": 10000,
|
18 |
+
"connectSocket": 10000,
|
19 |
+
"uploadFile": 10000,
|
20 |
+
"downloadFile": 10000
|
21 |
+
},
|
22 |
+
"requiredBackgroundModes": ["audio"],
|
23 |
+
"style": "v2",
|
24 |
+
"sitemapLocation": "sitemap.json"
|
25 |
+
}
|
app.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
-
|
2 |
-
from transformers import pipeline, AutoProcessor
|
3 |
from optimum.onnxruntime import ORTModelForSpeechSeq2Seq
|
4 |
import torch
|
5 |
import os
|
@@ -11,6 +10,10 @@ import uvicorn
|
|
11 |
import deepl
|
12 |
from dotenv import load_dotenv
|
13 |
import soundfile as sf
|
|
|
|
|
|
|
|
|
14 |
|
15 |
# --- Load environment variables and initialize DeepL ---
|
16 |
load_dotenv()
|
@@ -20,157 +23,188 @@ deepl_translator = None
|
|
20 |
if DEEPL_AUTH_KEY:
|
21 |
try:
|
22 |
deepl_translator = deepl.Translator(DEEPL_AUTH_KEY)
|
23 |
-
|
24 |
except Exception as e:
|
25 |
-
|
26 |
-
print("DeepL will be unavailable.")
|
27 |
else:
|
28 |
-
|
29 |
-
# --- End ---
|
30 |
-
|
31 |
|
32 |
-
#
|
33 |
-
|
34 |
|
35 |
-
# ASR Model
|
36 |
-
print("Loading optimized ASR model...")
|
37 |
asr_model_id = "openai/whisper-base"
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
#
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
def transcribe_audio(audio_bytes):
|
|
|
|
|
59 |
try:
|
60 |
-
# Use a temporary file to handle the audio bytes
|
61 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
62 |
tmp_file.write(audio_bytes)
|
63 |
audio_path = tmp_file.name
|
64 |
|
65 |
-
# Read the audio file and process it
|
66 |
audio_input, sample_rate = sf.read(audio_path)
|
67 |
-
|
68 |
-
# Ensure the audio is in the correct format (mono, 16kHz)
|
69 |
if audio_input.ndim > 1:
|
70 |
-
audio_input = audio_input.mean(axis=1)
|
71 |
-
if sample_rate != 16000:
|
72 |
-
# This is a placeholder for resampling. For now, we assume frontend sends 16kHz.
|
73 |
-
pass
|
74 |
|
75 |
-
# Process audio and generate token IDs
|
76 |
input_features = asr_processor(audio_input, sampling_rate=16000, return_tensors="pt").input_features
|
77 |
-
predicted_ids = asr_model.generate(input_features)
|
78 |
|
79 |
-
#
|
|
|
|
|
|
|
80 |
text = asr_processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
|
|
|
|
|
81 |
|
82 |
os.remove(audio_path)
|
83 |
return text, None
|
84 |
except Exception as e:
|
85 |
-
|
86 |
if 'audio_path' in locals() and os.path.exists(audio_path):
|
87 |
os.remove(audio_path)
|
88 |
return None, str(e)
|
89 |
|
90 |
def translate_text(text, source_lang, target_lang):
|
91 |
-
|
92 |
-
|
93 |
-
if source_lang == target_lang:
|
94 |
-
return text, None
|
95 |
-
|
96 |
-
# --- DeepL Hybrid Logic ---
|
97 |
-
if source_lang == 'zh' and target_lang == 'ja' and deepl_translator:
|
98 |
-
print("Attempting translation with DeepL for zh -> ja")
|
99 |
try:
|
100 |
-
|
|
|
|
|
101 |
return result.text, None
|
102 |
except Exception as e:
|
103 |
-
|
104 |
-
# --- End ---
|
105 |
-
|
106 |
-
key = f"{source_lang}-{target_lang}"
|
107 |
-
try:
|
108 |
-
if key in translators:
|
109 |
-
return translators[key](text)[0]['translation_text'], None
|
110 |
-
|
111 |
-
elif source_lang != 'en' and target_lang != 'en':
|
112 |
-
if f"{source_lang}-en" not in translators or f"en-{target_lang}" not in translators:
|
113 |
-
return None, f"Bridge translation route not supported: {source_lang}-en or en-{target_lang}"
|
114 |
-
|
115 |
-
print(f"Performing bridge translation: {source_lang} -> en -> {target_lang}")
|
116 |
-
english_text = translators[f"{source_lang}-en"](text)[0]['translation_text']
|
117 |
-
return translators[f"en-{target_lang}"](english_text)[0]['translation_text'], None
|
118 |
-
else:
|
119 |
-
return None, f"Translation route not supported: {key}"
|
120 |
-
except Exception as e:
|
121 |
-
return None, str(e)
|
122 |
|
123 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
app = FastAPI()
|
125 |
|
126 |
-
|
|
|
|
|
|
|
127 |
@app.post("/api/asr")
|
128 |
async def api_asr(request: Request):
|
129 |
-
json_data = await request.json()
|
130 |
-
audio_data_uri = json_data.get('audio_data_uri')
|
131 |
-
if not audio_data_uri:
|
132 |
-
return JSONResponse(status_code=400, content={"error": "No audio_data_uri provided"})
|
133 |
try:
|
134 |
-
|
135 |
-
|
136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
if error:
|
|
|
138 |
return JSONResponse(status_code=500, content={"error": f"ASR Error: {error}"})
|
139 |
-
|
|
|
|
|
|
|
|
|
140 |
except Exception as e:
|
141 |
-
|
|
|
142 |
|
143 |
@app.post("/api/translate")
|
144 |
async def api_translate(request: Request):
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
# Decode the token IDs to text
|
167 |
-
transcript = asr_processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
|
168 |
-
return transcript
|
169 |
-
|
170 |
-
gradio_ui = gr.Interface(fn=gradio_asr, inputs=gr.Audio(type="filepath"), outputs="text", title="ASR Debugger")
|
171 |
-
|
172 |
-
# 6. Mount Gradio app onto FastAPI
|
173 |
-
app = gr.mount_gradio_app(app, gradio_ui, path="/")
|
174 |
|
|
|
175 |
if __name__ == "__main__":
|
176 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
1 |
+
from transformers import AutoProcessor
|
|
|
2 |
from optimum.onnxruntime import ORTModelForSpeechSeq2Seq
|
3 |
import torch
|
4 |
import os
|
|
|
10 |
import deepl
|
11 |
from dotenv import load_dotenv
|
12 |
import soundfile as sf
|
13 |
+
import logging
|
14 |
+
|
15 |
+
# --- Basic Configuration ---
|
16 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
17 |
|
18 |
# --- Load environment variables and initialize DeepL ---
|
19 |
load_dotenv()
|
|
|
23 |
if DEEPL_AUTH_KEY:
|
24 |
try:
|
25 |
deepl_translator = deepl.Translator(DEEPL_AUTH_KEY)
|
26 |
+
logging.info("DeepL translator initialized successfully.")
|
27 |
except Exception as e:
|
28 |
+
logging.error(f"Error initializing DeepL translator: {e}")
|
|
|
29 |
else:
|
30 |
+
logging.warning("DEEPL_AUTH_KEY not found. DeepL will be unavailable.")
|
|
|
|
|
31 |
|
32 |
+
# --- Load Models ---
|
33 |
+
logging.info("Loading all models...")
|
34 |
|
35 |
+
# ASR Model
|
|
|
36 |
asr_model_id = "openai/whisper-base"
|
37 |
+
asr_model = None
|
38 |
+
asr_processor = None
|
39 |
+
try:
|
40 |
+
asr_model = ORTModelForSpeechSeq2Seq.from_pretrained(asr_model_id, provider="CPUExecutionProvider")
|
41 |
+
asr_processor = AutoProcessor.from_pretrained(asr_model_id)
|
42 |
+
|
43 |
+
# FINAL, CRITICAL FIX: The model's default config has a conflicting 'forced_decoder_ids'
|
44 |
+
# that clashes with the latest library versions. The library both requires this attribute
|
45 |
+
# to exist, but also requires it to be None to avoid a conflict.
|
46 |
+
if hasattr(asr_model.config, 'forced_decoder_ids'):
|
47 |
+
logging.info("Found conflicting 'forced_decoder_ids' in model config. Setting to None.")
|
48 |
+
asr_model.config.forced_decoder_ids = None
|
49 |
+
|
50 |
+
if hasattr(asr_model.generation_config, 'forced_decoder_ids'):
|
51 |
+
logging.info("Found conflicting 'forced_decoder_ids' in generation_config. Setting to None.")
|
52 |
+
asr_model.generation_config.forced_decoder_ids = None
|
53 |
+
|
54 |
+
logging.info("ASR model and processor loaded and configured successfully.")
|
55 |
+
except Exception as e:
|
56 |
+
logging.error(f"Fatal error loading ASR model: {e}", exc_info=True)
|
57 |
+
|
58 |
+
# Translation Pipelines
|
59 |
+
from transformers import pipeline
|
60 |
+
translators = {}
|
61 |
+
try:
|
62 |
+
translators = {
|
63 |
+
"en-zh": pipeline("translation", model="Helsinki-NLP/opus-mt-en-zh"),
|
64 |
+
"zh-en": pipeline("translation", model="Helsinki-NLP/opus-mt-zh-en"),
|
65 |
+
"en-ja": pipeline("translation", model="staka/fugumt-en-ja"),
|
66 |
+
"ja-en": pipeline("translation", model="Helsinki-NLP/opus-mt-ja-en"),
|
67 |
+
"en-ko": pipeline("translation", model="Helsinki-NLP/opus-mt-tc-big-en-ko"),
|
68 |
+
"ko-en": pipeline("translation", model="Helsinki-NLP/opus-mt-ko-en"),
|
69 |
+
}
|
70 |
+
logging.info("Translation models loaded successfully.")
|
71 |
+
except Exception as e:
|
72 |
+
logging.error(f"Failed to load translation models: {e}")
|
73 |
+
|
74 |
+
# --- Core Logic Functions ---
|
75 |
def transcribe_audio(audio_bytes):
|
76 |
+
if not asr_model or not asr_processor:
|
77 |
+
return None, "ASR model or processor is not available."
|
78 |
try:
|
|
|
79 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
80 |
tmp_file.write(audio_bytes)
|
81 |
audio_path = tmp_file.name
|
82 |
|
|
|
83 |
audio_input, sample_rate = sf.read(audio_path)
|
|
|
|
|
84 |
if audio_input.ndim > 1:
|
85 |
+
audio_input = audio_input.mean(axis=1)
|
|
|
|
|
|
|
86 |
|
|
|
87 |
input_features = asr_processor(audio_input, sampling_rate=16000, return_tensors="pt").input_features
|
|
|
88 |
|
89 |
+
# By setting forced_decoder_ids to None in the config, we can now safely
|
90 |
+
# let the generate function handle the task without conflicts.
|
91 |
+
predicted_ids = asr_model.generate(input_features, task="transcribe")
|
92 |
+
|
93 |
text = asr_processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
|
94 |
+
|
95 |
+
logging.info(f"ASR transcribed text: '{text}'")
|
96 |
|
97 |
os.remove(audio_path)
|
98 |
return text, None
|
99 |
except Exception as e:
|
100 |
+
logging.error(f"ASR transcription failed: {e}", exc_info=True)
|
101 |
if 'audio_path' in locals() and os.path.exists(audio_path):
|
102 |
os.remove(audio_path)
|
103 |
return None, str(e)
|
104 |
|
105 |
def translate_text(text, source_lang, target_lang):
|
106 |
+
# Priority 1: Use DeepL for specific, high-quality pairs if available
|
107 |
+
if deepl_translator and ((source_lang == 'zh' and target_lang == 'ja') or (source_lang == 'en' and target_lang == 'ja')):
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
try:
|
109 |
+
dl_source_lang = "ZH" if source_lang == 'zh' else "EN"
|
110 |
+
logging.info(f"Attempting DeepL translation for {source_lang} -> {target_lang}")
|
111 |
+
result = deepl_translator.translate_text(text, source_lang=dl_source_lang, target_lang="JA")
|
112 |
return result.text, None
|
113 |
except Exception as e:
|
114 |
+
logging.error(f"DeepL failed: {e}. Falling back to HF models.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
|
116 |
+
# Priority 2: Try direct HF translation
|
117 |
+
model_key = f"{source_lang}-{target_lang}"
|
118 |
+
translator = translators.get(model_key)
|
119 |
+
if translator:
|
120 |
+
try:
|
121 |
+
logging.info(f"Attempting direct HF translation for {model_key}")
|
122 |
+
translated_text = translator(text, max_length=512)[0]['translation_text']
|
123 |
+
return translated_text, None
|
124 |
+
except Exception as e:
|
125 |
+
logging.error(f"Direct HF translation for {model_key} failed: {e}", exc_info=True)
|
126 |
+
# Don't return here, allow fallback to pivot
|
127 |
+
|
128 |
+
# Priority 3: Try pivot translation via English
|
129 |
+
if source_lang != 'en' and target_lang != 'en':
|
130 |
+
to_en_key = f"{source_lang}-en"
|
131 |
+
from_en_key = f"en-{target_lang}"
|
132 |
+
translator_to_en = translators.get(to_en_key)
|
133 |
+
translator_from_en = translators.get(from_en_key)
|
134 |
+
|
135 |
+
if translator_to_en and translator_from_en:
|
136 |
+
try:
|
137 |
+
logging.info(f"Attempting pivot translation for {source_lang} -> en -> {target_lang}")
|
138 |
+
# Step 1: Source to English
|
139 |
+
english_text = translator_to_en(text, max_length=512)[0]['translation_text']
|
140 |
+
logging.info(f"Pivot step (to en) result: '{english_text}'")
|
141 |
+
# Step 2: English to Target
|
142 |
+
final_text = translator_from_en(english_text, max_length=512)[0]['translation_text']
|
143 |
+
logging.info(f"Pivot step (from en) result: '{final_text}'")
|
144 |
+
return final_text, None
|
145 |
+
except Exception as e:
|
146 |
+
logging.error(f"Pivot translation failed: {e}", exc_info=True)
|
147 |
+
|
148 |
+
# If all else fails
|
149 |
+
logging.warning(f"No translation path found for {source_lang} -> {target_lang}")
|
150 |
+
return None, f"No model available for {source_lang} to {target_lang}"
|
151 |
+
|
152 |
+
# --- FastAPI App ---
|
153 |
app = FastAPI()
|
154 |
|
155 |
+
@app.get("/")
|
156 |
+
def root():
|
157 |
+
return {"status": "ok", "message": "Translator API is running."}
|
158 |
+
|
159 |
@app.post("/api/asr")
|
160 |
async def api_asr(request: Request):
|
|
|
|
|
|
|
|
|
161 |
try:
|
162 |
+
body = await request.json()
|
163 |
+
audio_b64 = body.get('audio_base64')
|
164 |
+
if not audio_b64:
|
165 |
+
logging.error("Request is missing 'audio_base64'")
|
166 |
+
return JSONResponse(status_code=400, content={"error": "No audio_base64 found in request"})
|
167 |
+
|
168 |
+
audio_bytes = base64.b64decode(audio_b64)
|
169 |
+
|
170 |
+
text, error = transcribe_audio(audio_bytes)
|
171 |
+
|
172 |
if error:
|
173 |
+
logging.error(f"ASR transcription function returned an error: {error}")
|
174 |
return JSONResponse(status_code=500, content={"error": f"ASR Error: {error}"})
|
175 |
+
|
176 |
+
response_data = {"text": text}
|
177 |
+
logging.info(f"Returning ASR response: {response_data}")
|
178 |
+
return JSONResponse(content=response_data)
|
179 |
+
|
180 |
except Exception as e:
|
181 |
+
logging.error(f"Critical error in /api/asr endpoint: {e}", exc_info=True)
|
182 |
+
return JSONResponse(status_code=500, content={"error": str(e)})
|
183 |
|
184 |
@app.post("/api/translate")
|
185 |
async def api_translate(request: Request):
|
186 |
+
try:
|
187 |
+
body = await request.json()
|
188 |
+
text = body.get('text')
|
189 |
+
source_lang = body.get('source_lang')
|
190 |
+
target_lang = body.get('target_lang')
|
191 |
+
|
192 |
+
if not all([text, source_lang, target_lang]):
|
193 |
+
return JSONResponse(status_code=400, content={"error": "Missing parameters: text, source_lang, or target_lang"})
|
194 |
+
|
195 |
+
translated_text, error = translate_text(text, source_lang, target_lang)
|
196 |
+
|
197 |
+
if error:
|
198 |
+
return JSONResponse(status_code=500, content={"error": f"Translation Error: {error}"})
|
199 |
+
|
200 |
+
response_data = {"translated_text": translated_text}
|
201 |
+
logging.info(f"Returning translation response: {response_data}")
|
202 |
+
return JSONResponse(content=response_data)
|
203 |
+
|
204 |
+
except Exception as e:
|
205 |
+
logging.error(f"Error in /api/translate endpoint: {e}", exc_info=True)
|
206 |
+
return JSONResponse(status_code=500, content={"error": str(e)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
|
208 |
+
# --- Main Execution ---
|
209 |
if __name__ == "__main__":
|
210 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
asr.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import whisper
|
3 |
+
|
4 |
+
def transcribe_audio(file_path):
|
5 |
+
"""
|
6 |
+
Transcribes an audio file using Whisper.
|
7 |
+
"""
|
8 |
+
try:
|
9 |
+
# Load the base model. You can change this to 'tiny', 'small', 'medium', or 'large'
|
10 |
+
# depending on your server's performance and desired accuracy.
|
11 |
+
# 'base' is a good starting point.
|
12 |
+
model = whisper.load_model("base")
|
13 |
+
|
14 |
+
# Transcribe the audio file
|
15 |
+
result = model.transcribe(file_path)
|
16 |
+
|
17 |
+
# Return the transcribed text
|
18 |
+
return result["text"]
|
19 |
+
except Exception as e:
|
20 |
+
# Return the error message if something goes wrong
|
21 |
+
return f"Error during transcription: {str(e)}"
|
22 |
+
|
23 |
+
if __name__ == "__main__":
|
24 |
+
# The script expects exactly one argument: the audio file path
|
25 |
+
if len(sys.argv) != 2:
|
26 |
+
print("Usage: python asr.py <audio_file_path>")
|
27 |
+
sys.exit(1)
|
28 |
+
|
29 |
+
audio_file = sys.argv[1]
|
30 |
+
transcribed_text = transcribe_audio(audio_file)
|
31 |
+
print(transcribed_text)
|
asr_server.py
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import flask
|
2 |
+
from flask import request, jsonify
|
3 |
+
import logging
|
4 |
+
import numpy as np
|
5 |
+
import ffmpeg
|
6 |
+
from faster_whisper import WhisperModel
|
7 |
+
import os
|
8 |
+
|
9 |
+
# --- Configuration from Environment Variables ---
|
10 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "small")
|
11 |
+
CPU_THREADS = int(os.getenv("CPU_THREADS", "2"))
|
12 |
+
|
13 |
+
# --- Setup ---
|
14 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
15 |
+
|
16 |
+
# --- Load Model with Performance Optimizations ---
|
17 |
+
model = None
|
18 |
+
try:
|
19 |
+
logging.info(f"Loading faster-whisper model '{MODEL_NAME}' with {CPU_THREADS} threads...")
|
20 |
+
# 回退到兼容性最好的 int8 计算模式
|
21 |
+
model = WhisperModel(MODEL_NAME, device="cpu", compute_type="int8", cpu_threads=CPU_THREADS, num_workers=1)
|
22 |
+
logging.info(f"Whisper '{MODEL_NAME}' model loaded successfully.")
|
23 |
+
except Exception as e:
|
24 |
+
logging.error(f"Fatal error: Could not load model. {e}")
|
25 |
+
exit(1)
|
26 |
+
|
27 |
+
# --- Initialize Flask App ---
|
28 |
+
app = flask.Flask(__name__)
|
29 |
+
|
30 |
+
def load_audio_from_buffer(buffer, sr=16000):
|
31 |
+
"""Decodes audio from an in-memory buffer using ffmpeg."""
|
32 |
+
try:
|
33 |
+
out, _ = (
|
34 |
+
ffmpeg.input('pipe:', threads=0)
|
35 |
+
.output('pipe:', format='s16le', acodec='pcm_s16le', ac=1, ar=sr)
|
36 |
+
.run(input=buffer, capture_stdout=True, capture_stderr=True)
|
37 |
+
)
|
38 |
+
except ffmpeg.Error as e:
|
39 |
+
raise RuntimeError(f"Failed to load audio with ffmpeg: {e.stderr.decode()}") from e
|
40 |
+
return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
|
41 |
+
|
42 |
+
@app.route('/transcribe', methods=['POST'])
|
43 |
+
def transcribe_audio():
|
44 |
+
if 'audio' not in request.files:
|
45 |
+
return jsonify({"error": "No audio file part in the request"}), 400
|
46 |
+
|
47 |
+
file = request.files['audio']
|
48 |
+
if file.filename == '':
|
49 |
+
return jsonify({"error": "No selected file"}), 400
|
50 |
+
|
51 |
+
try:
|
52 |
+
audio_buffer = file.read()
|
53 |
+
audio_np = load_audio_from_buffer(audio_buffer)
|
54 |
+
|
55 |
+
source_lang_full = request.form.get('sourceLang')
|
56 |
+
lang_code = None
|
57 |
+
if source_lang_full:
|
58 |
+
lang_code = source_lang_full.split('-')[0]
|
59 |
+
logging.info(f"Client requested language: {lang_code}")
|
60 |
+
|
61 |
+
# 保留有���的性能优化: 减小 beam_size 并调整 VAD
|
62 |
+
segments, info = model.transcribe(
|
63 |
+
audio_np,
|
64 |
+
language=lang_code,
|
65 |
+
beam_size=2, # 从默认的5减小到2,显著降低计算量
|
66 |
+
vad_filter=True,
|
67 |
+
vad_parameters=dict(min_silence_duration_ms=700) # 稍微增加静音检测时长
|
68 |
+
)
|
69 |
+
|
70 |
+
transcript = "".join(segment.text for segment in segments).strip()
|
71 |
+
|
72 |
+
logging.info(f"Detected language '{info.language}' with probability {info.language_probability:.2f}")
|
73 |
+
logging.info(f"Transcription result: '{transcript}'")
|
74 |
+
return jsonify({"transcript": transcript})
|
75 |
+
|
76 |
+
except Exception as e:
|
77 |
+
logging.error(f"Error during transcription: {e}", exc_info=True)
|
78 |
+
return jsonify({"error": f"Transcription failed: {str(e)}"}), 500
|
ecosystem.config.js
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
apps: [
|
3 |
+
{
|
4 |
+
name: 'translator-server',
|
5 |
+
script: './server/server.js',
|
6 |
+
},
|
7 |
+
],
|
8 |
+
};
|
pages/index/.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
pages/index/index-original-全部依托于HF进行识别翻译的版本-可以运行.js
ADDED
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Page({
|
2 |
+
data: {
|
3 |
+
languages: {
|
4 |
+
'zh': { name: '中文', flag: 'cn' },
|
5 |
+
'en': { name: 'English', flag: 'us' },
|
6 |
+
'ja': { name: '日本語', flag: 'jp' },
|
7 |
+
'ko': { name: '한국어', flag: 'kr' }
|
8 |
+
},
|
9 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
10 |
+
sourceLang: 'zh',
|
11 |
+
targetLang: 'en',
|
12 |
+
transcript: '',
|
13 |
+
outputText: '',
|
14 |
+
isRecording: false,
|
15 |
+
sourceLanguages: [],
|
16 |
+
targetLanguages: [],
|
17 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
18 |
+
},
|
19 |
+
|
20 |
+
onLoad: function () {
|
21 |
+
this.initializeLanguages();
|
22 |
+
this.recorderManager = wx.getRecorderManager();
|
23 |
+
this.initRecorderManager();
|
24 |
+
},
|
25 |
+
|
26 |
+
initializeLanguages: function () {
|
27 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
28 |
+
const sourceLanguages = langCodes.map(code => ({
|
29 |
+
langCode: code,
|
30 |
+
name: languages[code].name,
|
31 |
+
flag: languages[code].flag,
|
32 |
+
selected: code === sourceLang
|
33 |
+
}));
|
34 |
+
const targetLanguages = langCodes.map(code => ({
|
35 |
+
langCode: code,
|
36 |
+
name: languages[code].name,
|
37 |
+
flag: languages[code].flag,
|
38 |
+
selected: code === targetLang
|
39 |
+
}));
|
40 |
+
this.setData({ sourceLanguages, targetLanguages });
|
41 |
+
},
|
42 |
+
|
43 |
+
selectSourceLanguage: function (e) {
|
44 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
45 |
+
this.setData({ sourceLang: newSourceLang }, () => {
|
46 |
+
this.initializeLanguages();
|
47 |
+
if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') {
|
48 |
+
this.translate(this.data.transcript);
|
49 |
+
}
|
50 |
+
});
|
51 |
+
},
|
52 |
+
|
53 |
+
selectTargetLanguage: function (e) {
|
54 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
55 |
+
this.setData({ targetLang: newTargetLang }, () => {
|
56 |
+
this.initializeLanguages();
|
57 |
+
if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') {
|
58 |
+
this.translate(this.data.transcript);
|
59 |
+
}
|
60 |
+
});
|
61 |
+
},
|
62 |
+
|
63 |
+
swapLanguages: function () {
|
64 |
+
let { sourceLang, targetLang, transcript, outputText } = this.data;
|
65 |
+
const tempLang = sourceLang;
|
66 |
+
sourceLang = targetLang;
|
67 |
+
targetLang = tempLang;
|
68 |
+
const tempText = transcript;
|
69 |
+
transcript = outputText;
|
70 |
+
outputText = tempText;
|
71 |
+
this.setData({ sourceLang, targetLang, transcript, outputText }, () => {
|
72 |
+
this.initializeLanguages();
|
73 |
+
if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') {
|
74 |
+
this.translate(this.data.transcript);
|
75 |
+
}
|
76 |
+
});
|
77 |
+
},
|
78 |
+
|
79 |
+
initRecorderManager: function () {
|
80 |
+
this.recorderManager.onStart(() => {
|
81 |
+
this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' });
|
82 |
+
});
|
83 |
+
|
84 |
+
this.recorderManager.onStop((res) => {
|
85 |
+
this.setData({ isRecording: false });
|
86 |
+
if (res.tempFilePath) {
|
87 |
+
this.uploadAudioForASR(res.tempFilePath);
|
88 |
+
} else {
|
89 |
+
this.setData({ transcript: '录音文件创建失败' });
|
90 |
+
}
|
91 |
+
});
|
92 |
+
|
93 |
+
this.recorderManager.onError(() => {
|
94 |
+
this.setData({ isRecording: false, transcript: '语音识别出错' });
|
95 |
+
});
|
96 |
+
},
|
97 |
+
|
98 |
+
startRecording: function () {
|
99 |
+
this.recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' });
|
100 |
+
},
|
101 |
+
|
102 |
+
stopRecording: function () {
|
103 |
+
this.recorderManager.stop();
|
104 |
+
},
|
105 |
+
|
106 |
+
uploadAudioForASR: function (filePath) {
|
107 |
+
this.setData({ transcript: '正在识别...' });
|
108 |
+
const fileSystemManager = wx.getFileSystemManager();
|
109 |
+
fileSystemManager.readFile({
|
110 |
+
filePath: filePath,
|
111 |
+
encoding: 'base64',
|
112 |
+
success: (res) => {
|
113 |
+
const base64Data = res.data;
|
114 |
+
const dataUri = 'data:audio/mp3;base64,' + base64Data;
|
115 |
+
|
116 |
+
wx.request({
|
117 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
118 |
+
method: 'POST',
|
119 |
+
header: { 'Content-Type': 'application/json' },
|
120 |
+
data: { "audio_data_uri": dataUri },
|
121 |
+
timeout: 60000,
|
122 |
+
success: (res) => {
|
123 |
+
if (res.statusCode === 200 && res.data && res.data.transcript) {
|
124 |
+
const transcript = res.data.transcript;
|
125 |
+
this.setData({ transcript: transcript });
|
126 |
+
this.translate(transcript);
|
127 |
+
} else {
|
128 |
+
this.setData({ transcript: '语音识别失败' });
|
129 |
+
console.error('ASR Error:', res.data.error);
|
130 |
+
}
|
131 |
+
},
|
132 |
+
fail: (err) => {
|
133 |
+
this.setData({ transcript: '语音识别请求失败' });
|
134 |
+
}
|
135 |
+
});
|
136 |
+
},
|
137 |
+
fail: (err) => {
|
138 |
+
this.setData({ transcript: '读取音频文件失败' });
|
139 |
+
}
|
140 |
+
});
|
141 |
+
},
|
142 |
+
|
143 |
+
translate: function (text) {
|
144 |
+
if (!text) return;
|
145 |
+
const { sourceLang, targetLang } = this.data;
|
146 |
+
if (sourceLang === targetLang) {
|
147 |
+
this.setData({ outputText: text });
|
148 |
+
return;
|
149 |
+
}
|
150 |
+
this.setData({ outputText: '正在翻译...' });
|
151 |
+
wx.request({
|
152 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
153 |
+
method: 'POST',
|
154 |
+
header: { 'Content-Type': 'application/json' },
|
155 |
+
data: {
|
156 |
+
"text": text,
|
157 |
+
"source_lang": sourceLang,
|
158 |
+
"target_lang": targetLang
|
159 |
+
},
|
160 |
+
timeout: 30000,
|
161 |
+
success: (res) => {
|
162 |
+
if (res.statusCode === 200 && res.data && res.data.translated_text) {
|
163 |
+
this.setData({ outputText: res.data.translated_text });
|
164 |
+
} else {
|
165 |
+
this.setData({ outputText: '翻译失败' });
|
166 |
+
console.error('Translate Error:', res.data.error);
|
167 |
+
}
|
168 |
+
},
|
169 |
+
fail: (err) => {
|
170 |
+
this.setData({ outputText: '翻译出错' });
|
171 |
+
}
|
172 |
+
});
|
173 |
+
}
|
174 |
+
});
|
pages/index/index.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
// FINAL VERSION:
|
2 |
|
3 |
// Helper function to show detailed errors
|
4 |
function showDetailedError(title, content) {
|
@@ -26,28 +26,24 @@ Page({
|
|
26 |
},
|
27 |
|
28 |
onLoad: function () {
|
29 |
-
// Use the working pattern: attach recorderManager to `this`
|
30 |
this.recorderManager = wx.getRecorderManager();
|
31 |
this.initRecorderManager();
|
32 |
-
|
33 |
-
// Use the improved, simpler language list setup
|
34 |
this.setData({
|
35 |
sourceLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key })),
|
36 |
targetLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key }))
|
37 |
});
|
38 |
},
|
39 |
|
40 |
-
// --- Language Selection & UI
|
41 |
selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }); },
|
42 |
selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }); },
|
43 |
swapLanguages: function () {
|
44 |
this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript });
|
45 |
},
|
46 |
|
47 |
-
// ---
|
48 |
initRecorderManager: function () {
|
49 |
this.recorderManager.onStart(() => {
|
50 |
-
// Correct pattern: Set UI state *inside* the onStart callback
|
51 |
this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' });
|
52 |
});
|
53 |
|
@@ -56,7 +52,6 @@ Page({
|
|
56 |
if (res.tempFilePath) {
|
57 |
this.uploadAudioForASR(res.tempFilePath);
|
58 |
} else {
|
59 |
-
// This case might happen if recording is too short
|
60 |
this.setData({ transcript: '录音时间太短或无效' });
|
61 |
}
|
62 |
});
|
@@ -67,7 +62,7 @@ Page({
|
|
67 |
});
|
68 |
},
|
69 |
|
70 |
-
// ---
|
71 |
handleRecordToggle: function() {
|
72 |
if (this.data.isRecording) {
|
73 |
this.stopRecording();
|
@@ -85,14 +80,14 @@ Page({
|
|
85 |
});
|
86 |
},
|
87 |
|
88 |
-
// ---
|
89 |
startRecording: function () {
|
90 |
const options = {
|
91 |
-
duration: 60000,
|
92 |
-
sampleRate: 16000,
|
93 |
-
numberOfChannels: 1,
|
94 |
-
encodeBitRate: 48000,
|
95 |
-
format: 'mp3'
|
96 |
};
|
97 |
this.recorderManager.start(options);
|
98 |
},
|
@@ -101,25 +96,41 @@ Page({
|
|
101 |
this.recorderManager.stop();
|
102 |
},
|
103 |
|
104 |
-
// ---
|
105 |
uploadAudioForASR: function (filePath) {
|
106 |
this.setData({ transcript: '正在识别...' });
|
107 |
wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => {
|
108 |
wx.request({
|
109 |
url: `${this.data.hfSpaceUrl}/api/asr`,
|
110 |
method: 'POST',
|
111 |
-
data: { "
|
112 |
-
timeout:
|
113 |
success: (asrRes) => {
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
} else {
|
119 |
-
|
|
|
|
|
120 |
}
|
121 |
},
|
122 |
-
fail: (err) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
});
|
124 |
}});
|
125 |
},
|
@@ -137,13 +148,17 @@ Page({
|
|
137 |
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
138 |
timeout: 45000,
|
139 |
success: (res) => {
|
140 |
-
if (res.statusCode === 200 && res.data.translated_text) {
|
141 |
this.setData({ outputText: res.data.translated_text });
|
142 |
} else {
|
143 |
-
|
|
|
144 |
}
|
145 |
},
|
146 |
-
fail: (err) =>
|
|
|
|
|
|
|
147 |
});
|
148 |
}
|
149 |
});
|
|
|
1 |
+
// FINAL VERSION: v30 - Robust error handling and UI updates
|
2 |
|
3 |
// Helper function to show detailed errors
|
4 |
function showDetailedError(title, content) {
|
|
|
26 |
},
|
27 |
|
28 |
onLoad: function () {
|
|
|
29 |
this.recorderManager = wx.getRecorderManager();
|
30 |
this.initRecorderManager();
|
|
|
|
|
31 |
this.setData({
|
32 |
sourceLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key })),
|
33 |
targetLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key }))
|
34 |
});
|
35 |
},
|
36 |
|
37 |
+
// --- Language Selection & UI ---
|
38 |
selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }); },
|
39 |
selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }); },
|
40 |
swapLanguages: function () {
|
41 |
this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript });
|
42 |
},
|
43 |
|
44 |
+
// --- Recorder Initialization ---
|
45 |
initRecorderManager: function () {
|
46 |
this.recorderManager.onStart(() => {
|
|
|
47 |
this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' });
|
48 |
});
|
49 |
|
|
|
52 |
if (res.tempFilePath) {
|
53 |
this.uploadAudioForASR(res.tempFilePath);
|
54 |
} else {
|
|
|
55 |
this.setData({ transcript: '录音时间太短或无效' });
|
56 |
}
|
57 |
});
|
|
|
62 |
});
|
63 |
},
|
64 |
|
65 |
+
// --- Record Button Handler ---
|
66 |
handleRecordToggle: function() {
|
67 |
if (this.data.isRecording) {
|
68 |
this.stopRecording();
|
|
|
80 |
});
|
81 |
},
|
82 |
|
83 |
+
// --- Start/Stop Recording ---
|
84 |
startRecording: function () {
|
85 |
const options = {
|
86 |
+
duration: 60000,
|
87 |
+
sampleRate: 16000,
|
88 |
+
numberOfChannels: 1,
|
89 |
+
encodeBitRate: 48000,
|
90 |
+
format: 'mp3'
|
91 |
};
|
92 |
this.recorderManager.start(options);
|
93 |
},
|
|
|
96 |
this.recorderManager.stop();
|
97 |
},
|
98 |
|
99 |
+
// --- ASR & Translation Flow ---
|
100 |
uploadAudioForASR: function (filePath) {
|
101 |
this.setData({ transcript: '正在识别...' });
|
102 |
wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => {
|
103 |
wx.request({
|
104 |
url: `${this.data.hfSpaceUrl}/api/asr`,
|
105 |
method: 'POST',
|
106 |
+
data: { "audio_base64": res.data },
|
107 |
+
timeout: 120000,
|
108 |
success: (asrRes) => {
|
109 |
+
console.log("ASR Response:", asrRes); // Log for debugging
|
110 |
+
|
111 |
+
if (asrRes.statusCode === 200 && asrRes.data && typeof asrRes.data.text !== 'undefined') {
|
112 |
+
const transcript = asrRes.data.text;
|
113 |
+
if (transcript) {
|
114 |
+
this.setData({ transcript });
|
115 |
+
this.translate(transcript);
|
116 |
+
} else {
|
117 |
+
// Handle successful response with empty transcript
|
118 |
+
this.setData({ transcript: '未能识别到语音,请重试。' });
|
119 |
+
}
|
120 |
} else {
|
121 |
+
// Handle non-200 responses or malformed data
|
122 |
+
this.setData({ transcript: '识别失败,请重试。' });
|
123 |
+
showDetailedError('语音识别失败', asrRes.data || '服务器返回异常');
|
124 |
}
|
125 |
},
|
126 |
+
fail: (err) => {
|
127 |
+
this.setData({ transcript: '识别请求失败。' });
|
128 |
+
if (err.errMsg && err.errMsg.includes('timeout')) {
|
129 |
+
showDetailedError('识别超时', '服务器处理时间过长,请稍后再试或尝试更短的语音。');
|
130 |
+
} else {
|
131 |
+
showDetailedError('识别请求失败', err);
|
132 |
+
}
|
133 |
+
}
|
134 |
});
|
135 |
}});
|
136 |
},
|
|
|
148 |
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
149 |
timeout: 45000,
|
150 |
success: (res) => {
|
151 |
+
if (res.statusCode === 200 && res.data && res.data.translated_text) {
|
152 |
this.setData({ outputText: res.data.translated_text });
|
153 |
} else {
|
154 |
+
this.setData({ outputText: '翻译失败' });
|
155 |
+
showDetailedError('翻译失败', res.data || '服务器返回异常');
|
156 |
}
|
157 |
},
|
158 |
+
fail: (err) => {
|
159 |
+
this.setData({ outputText: '翻译请求失败' });
|
160 |
+
showDetailedError('翻译请求失败', err);
|
161 |
+
}
|
162 |
});
|
163 |
}
|
164 |
});
|
pages/index/index.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
{}
|
pages/index/index.wxml
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!-- wechat-miniprogram-translator/index.wxml -->
|
2 |
+
<view class="container">
|
3 |
+
<!-- IO Boxes -->
|
4 |
+
<view class="io-container">
|
5 |
+
<!-- Source Box -->
|
6 |
+
<view class="io-box" id="source-box">
|
7 |
+
<view class="language-panel">
|
8 |
+
<view class="panel-label">您说的话 (选择语言)</view>
|
9 |
+
<view class="language-buttons">
|
10 |
+
<block wx:for="{{sourceLanguages}}" wx:key="langCode">
|
11 |
+
<view class="lang-button {{sourceLang === item.langCode ? 'selected' : ''}}" data-lang-code="{{item.langCode}}" bindtap="selectSourceLanguage">
|
12 |
+
<image src="https://flagcdn.com/w40/{{item.flag}}.png" alt="{{item.name}}" class="flag-icon"></image>
|
13 |
+
</view>
|
14 |
+
</block>
|
15 |
+
</view>
|
16 |
+
</view>
|
17 |
+
<scroll-view scroll-y class="text-content" id="transcript">{{transcript}}</scroll-view>
|
18 |
+
</view>
|
19 |
+
|
20 |
+
<!-- Swap Button -->
|
21 |
+
<view class="swap-controls">
|
22 |
+
<button id="swap-btn" title="交换语言" bindtap="swapLanguages">
|
23 |
+
<text>🔄</text> <!-- Swap icon -->
|
24 |
+
</button>
|
25 |
+
</view>
|
26 |
+
|
27 |
+
<!-- Target Box -->
|
28 |
+
<view class="io-box" id="target-box">
|
29 |
+
<view class="language-panel">
|
30 |
+
<view class="panel-label">翻译成 (选择语言)</view>
|
31 |
+
<view class="language-buttons">
|
32 |
+
<block wx:for="{{targetLanguages}}" wx:key="langCode">
|
33 |
+
<view class="lang-button {{targetLang === item.langCode ? 'selected' : ''}}" data-lang-code="{{item.langCode}}" bindtap="selectTargetLanguage">
|
34 |
+
<image src="https://flagcdn.com/w40/{{item.flag}}.png" alt="{{item.name}}" class="flag-icon"></image>
|
35 |
+
</view>
|
36 |
+
</block>
|
37 |
+
</view>
|
38 |
+
</view>
|
39 |
+
<scroll-view scroll-y class="text-content" id="output-text">{{outputText}}</scroll-view>
|
40 |
+
</view>
|
41 |
+
</view>
|
42 |
+
</view>
|
43 |
+
|
44 |
+
<!-- Floating Mic Button -->
|
45 |
+
<view class="mic-container">
|
46 |
+
<button id="mic-btn" title="按住说话" class="{{isRecording ? 'recording' : ''}}" bindtouchstart="handleRecordToggle" bindtouchend="stopRecording">
|
47 |
+
<text>🎤</text> <!-- Microphone icon -->
|
48 |
+
</button>
|
49 |
+
</view>
|
pages/index/index.wxss
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* wechat-miniprogram-translator/index.wxss */
|
2 |
+
:root {
|
3 |
+
--background-color: #f0f2f5;
|
4 |
+
--card-bg: #ffffff;
|
5 |
+
--primary-text: #1c1e21;
|
6 |
+
--secondary-text: #606770;
|
7 |
+
--border-color: #e0e0e0;
|
8 |
+
--accent-color: #007bff;
|
9 |
+
--mic-button-bg: #28a745;
|
10 |
+
--mic-recording-bg: #dc3545;
|
11 |
+
/* Note: WeChat Mini Program does not fully support custom font families directly via CSS import. */
|
12 |
+
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
13 |
+
}
|
14 |
+
page {
|
15 |
+
height: 100%;
|
16 |
+
background-color: var(--background-color);
|
17 |
+
}
|
18 |
+
body {
|
19 |
+
/* In Mini Program, use 'page' for root styling, 'body' is not applicable */
|
20 |
+
}
|
21 |
+
.container {
|
22 |
+
width: 100%;
|
23 |
+
max-width: 500px; /* Optimized for mobile width */
|
24 |
+
margin: 0 auto;
|
25 |
+
display: flex;
|
26 |
+
flex-direction: column;
|
27 |
+
gap: 1rem;
|
28 |
+
height: 100vh; /* Use 100vh for full height in Mini Program */
|
29 |
+
padding: 1rem;
|
30 |
+
box-sizing: border-box;
|
31 |
+
}
|
32 |
+
|
33 |
+
.io-container {
|
34 |
+
display: flex;
|
35 |
+
flex-direction: column;
|
36 |
+
gap: 100rpx; /* Adjust this value as needed */
|
37 |
+
}
|
38 |
+
.language-panel {
|
39 |
+
background-color: var(--card-bg);
|
40 |
+
padding: 0.75rem;
|
41 |
+
border-radius: 12px;
|
42 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
43 |
+
}
|
44 |
+
.panel-label {
|
45 |
+
font-size: 0.9rem;
|
46 |
+
font-weight: 600;
|
47 |
+
margin-bottom: 0.75rem;
|
48 |
+
text-align: center;
|
49 |
+
color: var(--primary-text);
|
50 |
+
}
|
51 |
+
.language-buttons {
|
52 |
+
display: flex;
|
53 |
+
flex-wrap: wrap;
|
54 |
+
justify-content: center;
|
55 |
+
gap: 0.5rem;
|
56 |
+
}
|
57 |
+
.lang-button {
|
58 |
+
display: inline-flex; /* Use inline-flex to allow spacing */
|
59 |
+
flex-direction: column;
|
60 |
+
align-items: center;
|
61 |
+
justify-content: center;
|
62 |
+
width: 44px; /* Set a fixed width for the button */
|
63 |
+
height: 44px; /* Set a fixed height to make it a square */
|
64 |
+
margin: 5px;
|
65 |
+
padding: 0;
|
66 |
+
border: 2px solid transparent; /* Start with a transparent border */
|
67 |
+
border-radius: 50%; /* Make it a circle */
|
68 |
+
background-color: #f0f0f0;
|
69 |
+
transition: all 0.2s ease-in-out;
|
70 |
+
box-sizing: border-box; /* Ensure padding and border are inside the width/height */
|
71 |
+
}
|
72 |
+
|
73 |
+
.lang-button.selected {
|
74 |
+
border-color: #007aff; /* Blue border for selected state */
|
75 |
+
transform: scale(1.1); /* Slightly enlarge the selected button */
|
76 |
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
77 |
+
}
|
78 |
+
|
79 |
+
.flag-icon {
|
80 |
+
width: 32px; /* Adjust flag size to fit inside the circle */
|
81 |
+
height: 32px;
|
82 |
+
border-radius: 50%; /* Make the flag image itself circular */
|
83 |
+
object-fit: cover; /* Ensure the image covers the area nicely */
|
84 |
+
}
|
85 |
+
|
86 |
+
.mic-container {
|
87 |
+
position: fixed;
|
88 |
+
bottom: 30px;
|
89 |
+
left: 50%;
|
90 |
+
transform: translateX(-50%);
|
91 |
+
z-index: 100;
|
92 |
+
}
|
93 |
+
|
94 |
+
#mic-btn {
|
95 |
+
width: 70px;
|
96 |
+
height: 70px;
|
97 |
+
border-radius: 50%;
|
98 |
+
background-color: #007aff;
|
99 |
+
color: white;
|
100 |
+
display: flex;
|
101 |
+
align-items: center;
|
102 |
+
justify-content: center;
|
103 |
+
font-size: 30px;
|
104 |
+
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
|
105 |
+
transition: all 0.2s ease;
|
106 |
+
}
|
107 |
+
|
108 |
+
#mic-btn.recording {
|
109 |
+
background-color: #ff3b30; /* Red color when recording */
|
110 |
+
transform: scale(1.1);
|
111 |
+
box-shadow: 0 6px 16px rgba(255, 59, 48, 0.5);
|
112 |
+
}
|
pages/index/index_v10_old.js
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
|
3 |
+
Page({
|
4 |
+
data: {
|
5 |
+
languages: {
|
6 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
7 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
8 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
9 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
10 |
+
},
|
11 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
12 |
+
sourceLang: 'zh',
|
13 |
+
targetLang: 'en',
|
14 |
+
transcript: '',
|
15 |
+
outputText: '',
|
16 |
+
isRecording: false,
|
17 |
+
sourceLanguages: [],
|
18 |
+
targetLanguages: [],
|
19 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
20 |
+
},
|
21 |
+
|
22 |
+
onLoad: function () {
|
23 |
+
this.initializeLanguages();
|
24 |
+
this.initRecorderManager(); // Correctly initialize the manager within the page lifecycle
|
25 |
+
},
|
26 |
+
|
27 |
+
// --- Language Selection Logic ---
|
28 |
+
initializeLanguages: function () {
|
29 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
30 |
+
this.setData({
|
31 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
32 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
33 |
+
});
|
34 |
+
},
|
35 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
36 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
37 |
+
swapLanguages: function() {
|
38 |
+
this.setData({
|
39 |
+
sourceLang: this.data.targetLang,
|
40 |
+
targetLang: this.data.sourceLang,
|
41 |
+
transcript: this.data.outputText,
|
42 |
+
outputText: this.data.transcript
|
43 |
+
}, this.initializeLanguages);
|
44 |
+
},
|
45 |
+
|
46 |
+
// --- CORRECTED: Unified, Lifecycle-Aware Recording Logic ---
|
47 |
+
initRecorderManager: function() {
|
48 |
+
const manager = plugin.getRecordRecognitionManager();
|
49 |
+
|
50 |
+
manager.onStart = () => {
|
51 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
52 |
+
};
|
53 |
+
|
54 |
+
// CRITICAL: Only use onStop for final results to prevent race conditions.
|
55 |
+
manager.onStop = (res) => {
|
56 |
+
this.setData({ isRecording: false });
|
57 |
+
const { sourceLang, targetLang } = this.data;
|
58 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
59 |
+
|
60 |
+
if (isChineseEnglish) {
|
61 |
+
// Mode 1: Plugin handles both ASR and Translation
|
62 |
+
if (res.result) {
|
63 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
64 |
+
} else {
|
65 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
66 |
+
}
|
67 |
+
} else {
|
68 |
+
// Mode 2: Plugin handles ASR, then HF handles translation
|
69 |
+
if (res.tempFilePath) {
|
70 |
+
this.uploadAudioForASR(res.tempFilePath);
|
71 |
+
} else {
|
72 |
+
this.setData({ transcript: '录音文件获取失败' });
|
73 |
+
}
|
74 |
+
}
|
75 |
+
};
|
76 |
+
|
77 |
+
manager.onError = (res) => {
|
78 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
79 |
+
};
|
80 |
+
|
81 |
+
// CRITICAL: Save the manager instance to the page context
|
82 |
+
this.manager = manager;
|
83 |
+
},
|
84 |
+
|
85 |
+
startRecording: function () {
|
86 |
+
const { sourceLang, targetLang } = this.data;
|
87 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
88 |
+
|
89 |
+
this.setData({ isRecording: true });
|
90 |
+
|
91 |
+
if (isChineseEnglish) {
|
92 |
+
this.manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code });
|
93 |
+
} else {
|
94 |
+
this.manager.start({ lang: this.data.languages[sourceLang].code, translate: false });
|
95 |
+
}
|
96 |
+
},
|
97 |
+
|
98 |
+
stopRecording: function () {
|
99 |
+
this.manager.stop();
|
100 |
+
},
|
101 |
+
|
102 |
+
// --- HF Bridge Translation Flow (This part remains the same) ---
|
103 |
+
uploadAudioForASR: function (filePath) {
|
104 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
105 |
+
const fileSystemManager = wx.getFileSystemManager();
|
106 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
107 |
+
wx.request({
|
108 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
109 |
+
method: 'POST',
|
110 |
+
header: { 'Content-Type': 'application/json' },
|
111 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
112 |
+
timeout: 60000,
|
113 |
+
success: (asrRes) => {
|
114 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
115 |
+
const transcript = asrRes.data.transcript;
|
116 |
+
this.setData({ transcript });
|
117 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
118 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
119 |
+
},
|
120 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
121 |
+
});
|
122 |
+
}});
|
123 |
+
},
|
124 |
+
|
125 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
126 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
127 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
128 |
+
if (englishResult) {
|
129 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
130 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
131 |
+
if (finalResult) {
|
132 |
+
this.setData({ outputText: finalResult });
|
133 |
+
}
|
134 |
+
});
|
135 |
+
}
|
136 |
+
});
|
137 |
+
},
|
138 |
+
|
139 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
140 |
+
wx.request({
|
141 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
142 |
+
method: 'POST',
|
143 |
+
header: { 'Content-Type': 'application/json' },
|
144 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
145 |
+
timeout: 30000,
|
146 |
+
success: (res) => {
|
147 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
148 |
+
callback(res.data.translated_text);
|
149 |
+
} else {
|
150 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
151 |
+
callback(null);
|
152 |
+
}
|
153 |
+
},
|
154 |
+
fail: () => {
|
155 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
156 |
+
callback(null);
|
157 |
+
}
|
158 |
+
});
|
159 |
+
}
|
160 |
+
});
|
pages/index/index_v11_old.js
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
|
3 |
+
Page({
|
4 |
+
data: {
|
5 |
+
languages: {
|
6 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
7 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' }
|
8 |
+
},
|
9 |
+
langCodes: ['zh', 'en'], // Only CN and EN for this test version
|
10 |
+
sourceLang: 'zh',
|
11 |
+
targetLang: 'en',
|
12 |
+
transcript: '',
|
13 |
+
outputText: '',
|
14 |
+
isRecording: false,
|
15 |
+
sourceLanguages: [],
|
16 |
+
targetLanguages: []
|
17 |
+
},
|
18 |
+
|
19 |
+
onLoad: function () {
|
20 |
+
this.initializeLanguages();
|
21 |
+
this.initRecorderManager();
|
22 |
+
},
|
23 |
+
|
24 |
+
// --- Language Selection Logic (Simplified for CN/EN only) ---
|
25 |
+
initializeLanguages: function () {
|
26 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
27 |
+
this.setData({
|
28 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
29 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
30 |
+
});
|
31 |
+
},
|
32 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
33 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
34 |
+
swapLanguages: function() {
|
35 |
+
this.setData({
|
36 |
+
sourceLang: this.data.targetLang,
|
37 |
+
targetLang: this.data.sourceLang,
|
38 |
+
transcript: this.data.outputText,
|
39 |
+
outputText: this.data.transcript
|
40 |
+
}, this.initializeLanguages);
|
41 |
+
},
|
42 |
+
|
43 |
+
// --- Recorder Manager Initialization (Pure Plugin Mode) ---
|
44 |
+
initRecorderManager: function() {
|
45 |
+
const manager = plugin.getRecordRecognitionManager();
|
46 |
+
|
47 |
+
manager.onStart = () => {
|
48 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
49 |
+
};
|
50 |
+
|
51 |
+
// Only onStop is used for final results
|
52 |
+
manager.onStop = (res) => {
|
53 |
+
this.setData({ isRecording: false });
|
54 |
+
if (res.result) {
|
55 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
56 |
+
} else {
|
57 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
58 |
+
}
|
59 |
+
};
|
60 |
+
|
61 |
+
manager.onError = (res) => {
|
62 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
63 |
+
};
|
64 |
+
|
65 |
+
// CRITICAL: Save the manager instance to the page context
|
66 |
+
this.manager = manager;
|
67 |
+
},
|
68 |
+
|
69 |
+
// --- Recording Start/Stop (Pure Plugin Mode) ---
|
70 |
+
startRecording: function () {
|
71 |
+
const { sourceLang, targetLang } = this.data;
|
72 |
+
this.setData({ isRecording: true });
|
73 |
+
this.manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code });
|
74 |
+
},
|
75 |
+
|
76 |
+
stopRecording: function () {
|
77 |
+
this.manager.stop();
|
78 |
+
}
|
79 |
+
});
|
pages/index/index_v12_old.js
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
|
3 |
+
Page({
|
4 |
+
data: {
|
5 |
+
languages: {
|
6 |
+
'zh_CN': { name: '中文', flag: 'cn' },
|
7 |
+
'en_US': { name: 'English', flag: 'us' }
|
8 |
+
},
|
9 |
+
langCodes: ['zh_CN', 'en_US'], // Only CN and EN for this test version
|
10 |
+
sourceLang: 'zh_CN',
|
11 |
+
targetLang: 'en_US',
|
12 |
+
transcript: '',
|
13 |
+
outputText: '',
|
14 |
+
isRecording: false,
|
15 |
+
sourceLanguages: [],
|
16 |
+
targetLanguages: []
|
17 |
+
},
|
18 |
+
|
19 |
+
onLoad: function () {
|
20 |
+
this.initializeLanguages();
|
21 |
+
this.initRecorderManager();
|
22 |
+
},
|
23 |
+
|
24 |
+
// --- Language Selection Logic (Simplified for CN/EN only) ---
|
25 |
+
initializeLanguages: function () {
|
26 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
27 |
+
this.setData({
|
28 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
29 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
30 |
+
});
|
31 |
+
},
|
32 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
33 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
34 |
+
swapLanguages: function() {
|
35 |
+
this.setData({
|
36 |
+
sourceLang: this.data.targetLang,
|
37 |
+
targetLang: this.data.sourceLang,
|
38 |
+
transcript: this.data.outputText,
|
39 |
+
outputText: this.data.transcript
|
40 |
+
}, this.initializeLanguages);
|
41 |
+
},
|
42 |
+
|
43 |
+
// --- Recorder Manager Initialization (Pure Plugin Mode) ---
|
44 |
+
initRecorderManager: function() {
|
45 |
+
const manager = plugin.getRecordRecognitionManager();
|
46 |
+
|
47 |
+
manager.onStart = () => {
|
48 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
49 |
+
};
|
50 |
+
|
51 |
+
// Only onStop is used for final results
|
52 |
+
manager.onStop = (res) => {
|
53 |
+
this.setData({ isRecording: false });
|
54 |
+
if (res.result) {
|
55 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
56 |
+
} else {
|
57 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
58 |
+
}
|
59 |
+
};
|
60 |
+
|
61 |
+
manager.onError = (res) => {
|
62 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
63 |
+
};
|
64 |
+
|
65 |
+
// CRITICAL: Save the manager instance to the page context
|
66 |
+
this.manager = manager;
|
67 |
+
},
|
68 |
+
|
69 |
+
// --- Recording Start/Stop (Pure Plugin Mode) ---
|
70 |
+
startRecording: function () {
|
71 |
+
const { sourceLang, targetLang } = this.data;
|
72 |
+
this.setData({ isRecording: true });
|
73 |
+
// CORRECTED: Use trans_lang instead of lto
|
74 |
+
this.manager.start({
|
75 |
+
lang: this.data.languages[sourceLang].code,
|
76 |
+
trans_lang: this.data.languages[targetLang].code,
|
77 |
+
});
|
78 |
+
},
|
79 |
+
|
80 |
+
stopRecording: function () {
|
81 |
+
this.manager.stop();
|
82 |
+
}
|
83 |
+
});
|
pages/index/index_v13_old.js
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
const manager = plugin.getRecordRecognitionManager(); // CRITICAL: Global creation
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' }
|
9 |
+
},
|
10 |
+
langCodes: ['zh', 'en'], // Only CN and EN for this test version
|
11 |
+
sourceLang: 'zh',
|
12 |
+
targetLang: 'en',
|
13 |
+
transcript: '',
|
14 |
+
outputText: '',
|
15 |
+
isRecording: false,
|
16 |
+
sourceLanguages: [],
|
17 |
+
targetLanguages: []
|
18 |
+
},
|
19 |
+
|
20 |
+
onLoad: function () {
|
21 |
+
this.initializeLanguages();
|
22 |
+
this.initRecorderManager(); // Set up event listeners for the globally created manager
|
23 |
+
},
|
24 |
+
|
25 |
+
// --- Language Selection Logic (Simplified for CN/EN only) ---
|
26 |
+
initializeLanguages: function () {
|
27 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
28 |
+
this.setData({
|
29 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
30 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
31 |
+
});
|
32 |
+
},
|
33 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
34 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
35 |
+
swapLanguages: function() {
|
36 |
+
this.setData({
|
37 |
+
sourceLang: this.data.targetLang,
|
38 |
+
targetLang: this.data.sourceLang,
|
39 |
+
transcript: this.data.outputText,
|
40 |
+
outputText: this.data.transcript
|
41 |
+
}, this.initializeLanguages);
|
42 |
+
},
|
43 |
+
|
44 |
+
// --- Recorder Manager Initialization (Pure Plugin Mode) ---
|
45 |
+
initRecorderManager: function() {
|
46 |
+
// Event listeners are set on the globally created manager
|
47 |
+
manager.onStart = () => {
|
48 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
49 |
+
};
|
50 |
+
|
51 |
+
// Only onStop is used for final results
|
52 |
+
manager.onStop = (res) => {
|
53 |
+
this.setData({ isRecording: false });
|
54 |
+
if (res.result) {
|
55 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
56 |
+
} else {
|
57 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
58 |
+
}
|
59 |
+
};
|
60 |
+
|
61 |
+
manager.onError = (res) => {
|
62 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
63 |
+
};
|
64 |
+
},
|
65 |
+
|
66 |
+
// --- Recording Start/Stop (Pure Plugin Mode) ---
|
67 |
+
startRecording: function () {
|
68 |
+
const { sourceLang, targetLang } = this.data;
|
69 |
+
this.setData({ isRecording: true });
|
70 |
+
manager.start({
|
71 |
+
lang: this.data.languages[sourceLang].code,
|
72 |
+
trans_lang: this.data.languages[targetLang].code,
|
73 |
+
});
|
74 |
+
},
|
75 |
+
|
76 |
+
stopRecording: function () {
|
77 |
+
manager.stop();
|
78 |
+
}
|
79 |
+
});
|
pages/index/index_v14_同声传译API_only-可以运行_old.js
ADDED
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
const plugin = requirePlugin("WechatSI");
|
3 |
+
const manager = plugin.getRecordRecognitionManager();
|
4 |
+
|
5 |
+
Page({
|
6 |
+
data: {
|
7 |
+
languages: {
|
8 |
+
'zh_CN': { name: '中文', flag: 'cn' },
|
9 |
+
'en_US': { name: 'English', flag: 'us' },
|
10 |
+
'ja_JP': { name: '日本語', flag: 'jp' },
|
11 |
+
'ko_KR': { name: '한국어', flag: 'kr' }
|
12 |
+
},
|
13 |
+
langCodes: ['zh_CN', 'en_US', 'ja_JP', 'ko_KR'],
|
14 |
+
sourceLang: 'zh_CN',
|
15 |
+
targetLang: 'en_US',
|
16 |
+
transcript: '',
|
17 |
+
outputText: '',
|
18 |
+
isRecording: false,
|
19 |
+
sourceLanguages: [],
|
20 |
+
targetLanguages: []
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.initRecord();
|
26 |
+
},
|
27 |
+
|
28 |
+
initializeLanguages: function () {
|
29 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
30 |
+
const sourceLanguages = langCodes.map(code => ({
|
31 |
+
langCode: code,
|
32 |
+
name: languages[code].name,
|
33 |
+
flag: languages[code].flag,
|
34 |
+
selected: code === sourceLang
|
35 |
+
}));
|
36 |
+
const targetLanguages = langCodes.map(code => ({
|
37 |
+
langCode: code,
|
38 |
+
name: languages[code].name,
|
39 |
+
flag: languages[code].flag,
|
40 |
+
selected: code === targetLang
|
41 |
+
}));
|
42 |
+
this.setData({ sourceLanguages, targetLanguages });
|
43 |
+
},
|
44 |
+
|
45 |
+
selectSourceLanguage: function (e) {
|
46 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
47 |
+
this.setData({ sourceLang: newSourceLang }, this.initializeLanguages);
|
48 |
+
},
|
49 |
+
|
50 |
+
selectTargetLanguage: function (e) {
|
51 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
52 |
+
this.setData({ targetLang: newTargetLang }, this.initializeLanguages);
|
53 |
+
},
|
54 |
+
|
55 |
+
swapLanguages: function () {
|
56 |
+
const { sourceLang, targetLang } = this.data;
|
57 |
+
this.setData({
|
58 |
+
sourceLang: targetLang,
|
59 |
+
targetLang: sourceLang,
|
60 |
+
transcript: this.data.outputText,
|
61 |
+
outputText: this.data.transcript
|
62 |
+
}, this.initializeLanguages);
|
63 |
+
},
|
64 |
+
|
65 |
+
initRecord: function () {
|
66 |
+
manager.onStart = (res) => {
|
67 |
+
console.log("成功开始录音识别", res);
|
68 |
+
this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' });
|
69 |
+
};
|
70 |
+
|
71 |
+
manager.onRecognize = (res) => {
|
72 |
+
this.setData({ transcript: res.result });
|
73 |
+
};
|
74 |
+
|
75 |
+
manager.onStop = (res) => {
|
76 |
+
console.log("录音识别结束", res);
|
77 |
+
this.setData({ isRecording: false, transcript: res.result });
|
78 |
+
if (res.result) {
|
79 |
+
this.translate(res.result);
|
80 |
+
} else {
|
81 |
+
this.setData({ transcript: '未能识别到语音' });
|
82 |
+
}
|
83 |
+
};
|
84 |
+
|
85 |
+
manager.onError = (res) => {
|
86 |
+
console.error("录音识别错误", res);
|
87 |
+
this.setData({ isRecording: false, transcript: '语音识别出错' });
|
88 |
+
};
|
89 |
+
},
|
90 |
+
|
91 |
+
startRecording: function () {
|
92 |
+
const { sourceLang, targetLang } = this.data;
|
93 |
+
|
94 |
+
// 插件仅支持中英文
|
95 |
+
if ((sourceLang !== 'zh_CN' && sourceLang !== 'en_US') || (targetLang !== 'zh_CN' && targetLang !== 'en_US')) {
|
96 |
+
wx.showToast({
|
97 |
+
title: '插件仅支持中英文互译',
|
98 |
+
icon: 'none'
|
99 |
+
});
|
100 |
+
return;
|
101 |
+
}
|
102 |
+
|
103 |
+
manager.start({
|
104 |
+
lang: sourceLang,
|
105 |
+
trans_lang: targetLang,
|
106 |
+
});
|
107 |
+
},
|
108 |
+
|
109 |
+
stopRecording: function () {
|
110 |
+
manager.stop();
|
111 |
+
},
|
112 |
+
|
113 |
+
translate: function (text) {
|
114 |
+
const { sourceLang, targetLang } = this.data;
|
115 |
+
if (sourceLang === targetLang) {
|
116 |
+
this.setData({ outputText: text });
|
117 |
+
return;
|
118 |
+
}
|
119 |
+
|
120 |
+
this.setData({ outputText: '正在翻译...' });
|
121 |
+
|
122 |
+
plugin.translate({
|
123 |
+
lfrom: sourceLang,
|
124 |
+
lto: targetLang,
|
125 |
+
content: text,
|
126 |
+
success: (res) => {
|
127 |
+
if (res.retcode === 0) {
|
128 |
+
this.setData({ outputText: res.result });
|
129 |
+
} else {
|
130 |
+
console.error('翻译失败', res);
|
131 |
+
this.setData({ outputText: '翻译失败' });
|
132 |
+
}
|
133 |
+
},
|
134 |
+
fail: (err) => {
|
135 |
+
console.error('翻译接口调用失败', err);
|
136 |
+
this.setData({ outputText: '翻译出错' });
|
137 |
+
}
|
138 |
+
});
|
139 |
+
}
|
140 |
+
});
|
141 |
+
|
142 |
+
/*
|
143 |
+
--- Original index.v13.js content (commented out) ---
|
144 |
+
const plugin = requirePlugin('WechatSI');
|
145 |
+
const manager = plugin.getRecordRecognitionManager(); // CRITICAL: Global creation
|
146 |
+
|
147 |
+
Page({
|
148 |
+
data: {
|
149 |
+
languages: {
|
150 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
151 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' }
|
152 |
+
},
|
153 |
+
langCodes: ['zh', 'en'], // Only CN and EN for this test version
|
154 |
+
sourceLang: 'zh',
|
155 |
+
targetLang: 'en',
|
156 |
+
transcript: '',
|
157 |
+
outputText: '',
|
158 |
+
isRecording: false,
|
159 |
+
sourceLanguages: [],
|
160 |
+
targetLanguages: []
|
161 |
+
},
|
162 |
+
|
163 |
+
onLoad: function () {
|
164 |
+
this.initializeLanguages();
|
165 |
+
this.initRecorderManager(); // Set up event listeners for the globally created manager
|
166 |
+
},
|
167 |
+
|
168 |
+
// --- Language Selection Logic (Simplified for CN/EN only) ---
|
169 |
+
initializeLanguages: function () {
|
170 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
171 |
+
this.setData({
|
172 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
173 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
174 |
+
});
|
175 |
+
},
|
176 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
177 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
178 |
+
swapLanguages: function() {
|
179 |
+
this.setData({
|
180 |
+
sourceLang: this.data.targetLang,
|
181 |
+
targetLang: this.data.sourceLang,
|
182 |
+
transcript: this.data.outputText,
|
183 |
+
outputText: this.data.transcript
|
184 |
+
}, this.initializeLanguages);
|
185 |
+
},
|
186 |
+
|
187 |
+
// --- Recorder Manager Initialization (Pure Plugin Mode) ---
|
188 |
+
initRecorderManager: function() {
|
189 |
+
// Event listeners are set on the globally created manager
|
190 |
+
manager.onStart = () => {
|
191 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
192 |
+
};
|
193 |
+
|
194 |
+
// Only onStop is used for final results
|
195 |
+
manager.onStop = (res) => {
|
196 |
+
this.setData({ isRecording: false });
|
197 |
+
if (res.result) {
|
198 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
199 |
+
} else {
|
200 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
201 |
+
}
|
202 |
+
};
|
203 |
+
|
204 |
+
manager.onError = (res) => {
|
205 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
206 |
+
};
|
207 |
+
},
|
208 |
+
|
209 |
+
// --- Recording Start/Stop (Pure Plugin Mode) ---
|
210 |
+
startRecording: function () {
|
211 |
+
const { sourceLang, targetLang } = this.data;
|
212 |
+
this.setData({ isRecording: true });
|
213 |
+
manager.start({
|
214 |
+
lang: this.data.languages[sourceLang].code,
|
215 |
+
trans_lang: this.data.languages[targetLang].code,
|
216 |
+
});
|
217 |
+
},
|
218 |
+
|
219 |
+
stopRecording: function () {
|
220 |
+
manager.stop();
|
221 |
+
}
|
222 |
+
});
|
223 |
+
*/
|
pages/index/index_v15_old.js
ADDED
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const manager = plugin.getRecordRecognitionManager();
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
9 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
10 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
11 |
+
},
|
12 |
+
langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic
|
13 |
+
sourceLang: 'zh',
|
14 |
+
targetLang: 'en',
|
15 |
+
transcript: '',
|
16 |
+
outputText: '',
|
17 |
+
isRecording: false,
|
18 |
+
sourceLanguages: [],
|
19 |
+
targetLanguages: [],
|
20 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.initRecordManager();
|
26 |
+
},
|
27 |
+
|
28 |
+
// --- Language Selection Logic ---
|
29 |
+
initializeLanguages: function () {
|
30 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
31 |
+
this.setData({
|
32 |
+
sourceLanguages: langCodes.map(c => ({
|
33 |
+
langCode: c,
|
34 |
+
name: languages[c].name,
|
35 |
+
flag: languages[c].flag,
|
36 |
+
selected: c === sourceLang
|
37 |
+
})),
|
38 |
+
targetLanguages: langCodes.map(c => ({
|
39 |
+
langCode: c,
|
40 |
+
name: languages[c].name,
|
41 |
+
flag: languages[c].flag,
|
42 |
+
selected: c === targetLang
|
43 |
+
}))
|
44 |
+
});
|
45 |
+
},
|
46 |
+
selectSourceLanguage: function (e) {
|
47 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
48 |
+
this.setData({ sourceLang: newSourceLang }, this.initializeLanguages);
|
49 |
+
},
|
50 |
+
selectTargetLanguage: function (e) {
|
51 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
52 |
+
this.setData({ targetLang: newTargetLang }, this.initializeLanguages);
|
53 |
+
},
|
54 |
+
swapLanguages: function () {
|
55 |
+
const { sourceLang, targetLang } = this.data;
|
56 |
+
this.setData({
|
57 |
+
sourceLang: targetLang,
|
58 |
+
targetLang: sourceLang,
|
59 |
+
transcript: this.data.outputText,
|
60 |
+
outputText: this.data.transcript
|
61 |
+
}, this.initializeLanguages);
|
62 |
+
},
|
63 |
+
|
64 |
+
// --- Recorder Manager Initialization (Hybrid Mode) ---
|
65 |
+
initRecordManager: function () {
|
66 |
+
manager.onStart = () => {
|
67 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
68 |
+
};
|
69 |
+
|
70 |
+
manager.onRecognize = (res) => {
|
71 |
+
// For plugin mode, this updates transcript in real-time
|
72 |
+
// For HF mode, we only care about the final result in onStop
|
73 |
+
const { sourceLang, targetLang } = this.data;
|
74 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
75 |
+
if (isChineseEnglish) {
|
76 |
+
this.setData({ transcript: res.result });
|
77 |
+
}
|
78 |
+
};
|
79 |
+
|
80 |
+
manager.onStop = (res) => {
|
81 |
+
this.setData({ isRecording: false });
|
82 |
+
const { sourceLang, targetLang } = this.data;
|
83 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
84 |
+
|
85 |
+
if (isChineseEnglish) {
|
86 |
+
// Mode 1: Plugin handles both ASR and Translation
|
87 |
+
if (res.result) {
|
88 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
89 |
+
} else {
|
90 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
91 |
+
}
|
92 |
+
} else {
|
93 |
+
// Mode 2: Plugin handles ASR, then HF handles translation
|
94 |
+
if (res.tempFilePath) {
|
95 |
+
this.uploadAudioForASR(res.tempFilePath);
|
96 |
+
} else {
|
97 |
+
this.setData({ transcript: '录音文件获取失败' });
|
98 |
+
}
|
99 |
+
}
|
100 |
+
};
|
101 |
+
|
102 |
+
manager.onError = (res) => {
|
103 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
104 |
+
};
|
105 |
+
},
|
106 |
+
|
107 |
+
startRecording: function () {
|
108 |
+
const { sourceLang, targetLang } = this.data;
|
109 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
110 |
+
|
111 |
+
this.setData({ isRecording: true });
|
112 |
+
|
113 |
+
if (isChineseEnglish) {
|
114 |
+
// Use plugin for ASR and Translation
|
115 |
+
this.manager.start({
|
116 |
+
lang: this.data.languages[sourceLang].code,
|
117 |
+
trans_lang: this.data.languages[targetLang].code,
|
118 |
+
});
|
119 |
+
} else {
|
120 |
+
// Use plugin for ASR only, then HF for translation
|
121 |
+
this.manager.start({
|
122 |
+
lang: this.data.languages[sourceLang].code,
|
123 |
+
// No trans_lang here, as HF will handle translation
|
124 |
+
});
|
125 |
+
}
|
126 |
+
},
|
127 |
+
|
128 |
+
stopRecording: function () {
|
129 |
+
this.manager.stop();
|
130 |
+
},
|
131 |
+
|
132 |
+
// --- HF Bridge Translation Flow (for non-CN/EN pairs) ---
|
133 |
+
uploadAudioForASR: function (filePath) {
|
134 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
135 |
+
const fileSystemManager = wx.getFileSystemManager();
|
136 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
137 |
+
wx.request({
|
138 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
139 |
+
method: 'POST',
|
140 |
+
header: { 'Content-Type': 'application/json' },
|
141 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
142 |
+
timeout: 60000,
|
143 |
+
success: (asrRes) => {
|
144 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
145 |
+
const transcript = asrRes.data.transcript;
|
146 |
+
this.setData({ transcript });
|
147 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
148 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
149 |
+
},
|
150 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
151 |
+
});
|
152 |
+
}});
|
153 |
+
},
|
154 |
+
|
155 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
156 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
157 |
+
// Step 1: Translate source (e.g., JA) to English via HF
|
158 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
159 |
+
if (englishResult) {
|
160 |
+
// Step 2: Translate English result to final target (e.g., ZH) via HF
|
161 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
162 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
163 |
+
if (finalResult) {
|
164 |
+
this.setData({ outputText: finalResult });
|
165 |
+
}
|
166 |
+
});
|
167 |
+
}
|
168 |
+
});
|
169 |
+
},
|
170 |
+
|
171 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
172 |
+
wx.request({
|
173 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
174 |
+
method: 'POST',
|
175 |
+
header: { 'Content-Type': 'application/json' },
|
176 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
177 |
+
timeout: 30000,
|
178 |
+
success: (res) => {
|
179 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
180 |
+
callback(res.data.translated_text);
|
181 |
+
} else {
|
182 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
183 |
+
callback(null);
|
184 |
+
}
|
185 |
+
},
|
186 |
+
fail: () => {
|
187 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
188 |
+
callback(null);
|
189 |
+
}
|
190 |
+
});
|
191 |
+
}
|
192 |
+
});
|
pages/index/index_v16_old.js
ADDED
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const manager = plugin.getRecordRecognitionManager();
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
9 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
10 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
11 |
+
},
|
12 |
+
langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic
|
13 |
+
sourceLang: 'zh',
|
14 |
+
targetLang: 'en',
|
15 |
+
transcript: '',
|
16 |
+
outputText: '',
|
17 |
+
isRecording: false,
|
18 |
+
sourceLanguages: [],
|
19 |
+
targetLanguages: [],
|
20 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.initRecordManager();
|
26 |
+
},
|
27 |
+
|
28 |
+
// --- Language Selection Logic ---
|
29 |
+
initializeLanguages: function () {
|
30 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
31 |
+
this.setData({
|
32 |
+
sourceLanguages: langCodes.map(c => ({
|
33 |
+
langCode: c,
|
34 |
+
name: languages[c].name,
|
35 |
+
flag: languages[c].flag,
|
36 |
+
selected: c === sourceLang
|
37 |
+
})),
|
38 |
+
targetLanguages: langCodes.map(c => ({
|
39 |
+
langCode: c,
|
40 |
+
name: languages[c].name,
|
41 |
+
flag: languages[c].flag,
|
42 |
+
selected: c === targetLang
|
43 |
+
}))
|
44 |
+
});
|
45 |
+
},
|
46 |
+
selectSourceLanguage: function (e) {
|
47 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
48 |
+
this.setData({ sourceLang: newSourceLang }, this.initializeLanguages);
|
49 |
+
},
|
50 |
+
selectTargetLanguage: function (e) {
|
51 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
52 |
+
this.setData({ targetLang: newTargetLang }, this.initializeLanguages);
|
53 |
+
},
|
54 |
+
swapLanguages: function () {
|
55 |
+
const { sourceLang, targetLang } = this.data;
|
56 |
+
this.setData({
|
57 |
+
sourceLang: targetLang,
|
58 |
+
targetLang: sourceLang,
|
59 |
+
transcript: this.data.outputText,
|
60 |
+
outputText: this.data.transcript
|
61 |
+
}, this.initializeLanguages);
|
62 |
+
},
|
63 |
+
|
64 |
+
// --- Recorder Manager Initialization (Hybrid Mode) ---
|
65 |
+
initRecordManager: function () {
|
66 |
+
manager.onStart = () => {
|
67 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
68 |
+
};
|
69 |
+
|
70 |
+
manager.onRecognize = (res) => {
|
71 |
+
// For plugin mode, this updates transcript in real-time
|
72 |
+
// For HF mode, we only care about the final result in onStop
|
73 |
+
const { sourceLang, targetLang } = this.data;
|
74 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
75 |
+
if (isChineseEnglish) {
|
76 |
+
this.setData({ transcript: res.result });
|
77 |
+
}
|
78 |
+
};
|
79 |
+
|
80 |
+
manager.onStop = (res) => {
|
81 |
+
this.setData({ isRecording: false });
|
82 |
+
const { sourceLang, targetLang } = this.data;
|
83 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
84 |
+
|
85 |
+
if (isChineseEnglish) {
|
86 |
+
// Mode 1: Plugin handles both ASR and Translation
|
87 |
+
if (res.result) {
|
88 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
89 |
+
} else {
|
90 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
91 |
+
}
|
92 |
+
} else {
|
93 |
+
// Mode 2: Plugin handles ASR, then HF handles translation
|
94 |
+
if (res.tempFilePath) {
|
95 |
+
this.uploadAudioForASR(res.tempFilePath);
|
96 |
+
} else {
|
97 |
+
this.setData({ transcript: '录音文件获取失败' });
|
98 |
+
}
|
99 |
+
}
|
100 |
+
};
|
101 |
+
|
102 |
+
manager.onError = (res) => {
|
103 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
104 |
+
};
|
105 |
+
},
|
106 |
+
|
107 |
+
startRecording: function () {
|
108 |
+
const { sourceLang, targetLang } = this.data;
|
109 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
110 |
+
|
111 |
+
this.setData({ isRecording: true });
|
112 |
+
|
113 |
+
if (isChineseEnglish) {
|
114 |
+
// Use plugin for ASR and Translation
|
115 |
+
manager.start({
|
116 |
+
lang: this.data.languages[sourceLang].code,
|
117 |
+
trans_lang: this.data.languages[targetLang].code,
|
118 |
+
});
|
119 |
+
} else {
|
120 |
+
// Use plugin for ASR only, then HF for translation
|
121 |
+
manager.start({
|
122 |
+
lang: this.data.languages[sourceLang].code,
|
123 |
+
// No trans_lang here, as HF will handle translation
|
124 |
+
});
|
125 |
+
}
|
126 |
+
},
|
127 |
+
|
128 |
+
stopRecording: function () {
|
129 |
+
manager.stop();
|
130 |
+
},
|
131 |
+
|
132 |
+
// --- HF Bridge Translation Flow (for non-CN/EN pairs) ---
|
133 |
+
uploadAudioForASR: function (filePath) {
|
134 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
135 |
+
const fileSystemManager = wx.getFileSystemManager();
|
136 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
137 |
+
wx.request({
|
138 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
139 |
+
method: 'POST',
|
140 |
+
header: { 'Content-Type': 'application/json' },
|
141 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
142 |
+
timeout: 60000,
|
143 |
+
success: (asrRes) => {
|
144 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
145 |
+
const transcript = asrRes.data.transcript;
|
146 |
+
this.setData({ transcript });
|
147 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
148 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
149 |
+
},
|
150 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
151 |
+
});
|
152 |
+
}});
|
153 |
+
},
|
154 |
+
|
155 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
156 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
157 |
+
// Step 1: Translate source (e.g., JA) to English via HF
|
158 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
159 |
+
if (englishResult) {
|
160 |
+
// Step 2: Translate English result to final target (e.g., ZH) via HF
|
161 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
162 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
163 |
+
if (finalResult) {
|
164 |
+
this.setData({ outputText: finalResult });
|
165 |
+
}
|
166 |
+
});
|
167 |
+
}
|
168 |
+
});
|
169 |
+
},
|
170 |
+
|
171 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
172 |
+
wx.request({
|
173 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
174 |
+
method: 'POST',
|
175 |
+
header: { 'Content-Type': 'application/json' },
|
176 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
177 |
+
timeout: 30000,
|
178 |
+
success: (res) => {
|
179 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
180 |
+
callback(res.data.translated_text);
|
181 |
+
} else {
|
182 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
183 |
+
callback(null);
|
184 |
+
}
|
185 |
+
},
|
186 |
+
fail: () => {
|
187 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
188 |
+
callback(null);
|
189 |
+
}
|
190 |
+
});
|
191 |
+
}
|
192 |
+
});
|
pages/index/index_v17_old.js
ADDED
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const manager = plugin.getRecordRecognitionManager();
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
9 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
10 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
11 |
+
},
|
12 |
+
langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic
|
13 |
+
sourceLang: 'zh',
|
14 |
+
targetLang: 'en',
|
15 |
+
transcript: '',
|
16 |
+
outputText: '',
|
17 |
+
isRecording: false,
|
18 |
+
sourceLanguages: [],
|
19 |
+
targetLanguages: [],
|
20 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.initRecordManager();
|
26 |
+
},
|
27 |
+
|
28 |
+
// --- Language Selection Logic ---
|
29 |
+
initializeLanguages: function () {
|
30 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
31 |
+
this.setData({
|
32 |
+
sourceLanguages: langCodes.map(c => ({
|
33 |
+
langCode: c,
|
34 |
+
name: languages[c].name,
|
35 |
+
flag: languages[c].flag,
|
36 |
+
selected: c === sourceLang
|
37 |
+
})),
|
38 |
+
targetLanguages: langCodes.map(c => ({
|
39 |
+
langCode: c,
|
40 |
+
name: languages[c].name,
|
41 |
+
flag: languages[c].flag,
|
42 |
+
selected: c === targetLang
|
43 |
+
}))
|
44 |
+
});
|
45 |
+
},
|
46 |
+
selectSourceLanguage: function (e) {
|
47 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
48 |
+
this.setData({ sourceLang: newSourceLang }, this.initializeLanguages);
|
49 |
+
},
|
50 |
+
selectTargetLanguage: function (e) {
|
51 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
52 |
+
this.setData({ targetLang: newTargetLang }, this.initializeLanguages);
|
53 |
+
},
|
54 |
+
swapLanguages: function () {
|
55 |
+
const { sourceLang, targetLang } = this.data;
|
56 |
+
this.setData({
|
57 |
+
sourceLang: targetLang,
|
58 |
+
targetLang: sourceLang,
|
59 |
+
transcript: this.data.outputText,
|
60 |
+
outputText: this.data.transcript
|
61 |
+
}, this.initializeLanguages);
|
62 |
+
},
|
63 |
+
|
64 |
+
// --- Recorder Manager Initialization (Hybrid Mode) ---
|
65 |
+
initRecordManager: function () {
|
66 |
+
manager.onStart = () => {
|
67 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
68 |
+
};
|
69 |
+
|
70 |
+
manager.onRecognize = (res) => {
|
71 |
+
// For plugin mode, this updates transcript in real-time
|
72 |
+
// For HF mode, we only care about the final result in onStop
|
73 |
+
const { sourceLang, targetLang } = this.data;
|
74 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
75 |
+
if (isChineseEnglish) {
|
76 |
+
this.setData({ transcript: res.result });
|
77 |
+
}
|
78 |
+
};
|
79 |
+
|
80 |
+
manager.onStop = (res) => {
|
81 |
+
this.setData({ isRecording: false });
|
82 |
+
const { sourceLang, targetLang } = this.data;
|
83 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
84 |
+
|
85 |
+
if (isChineseEnglish) {
|
86 |
+
// Mode 1: Plugin handles ASR, then we explicitly call plugin.translate
|
87 |
+
if (res.result) {
|
88 |
+
this.setData({ transcript: res.result });
|
89 |
+
this.translate(res.result); // CRITICAL: Explicitly call translate
|
90 |
+
} else {
|
91 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
92 |
+
}
|
93 |
+
} else {
|
94 |
+
// Mode 2: Plugin handles ASR, then HF handles translation
|
95 |
+
if (res.tempFilePath) {
|
96 |
+
this.uploadAudioForASR(res.tempFilePath);
|
97 |
+
} else {
|
98 |
+
this.setData({ transcript: '录音文件获取失败' });
|
99 |
+
}
|
100 |
+
}
|
101 |
+
};
|
102 |
+
|
103 |
+
manager.onError = (res) => {
|
104 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
105 |
+
};
|
106 |
+
},
|
107 |
+
|
108 |
+
startRecording: function () {
|
109 |
+
const { sourceLang, targetLang } = this.data;
|
110 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
111 |
+
|
112 |
+
this.setData({ isRecording: true });
|
113 |
+
|
114 |
+
if (isChineseEnglish) {
|
115 |
+
// Use plugin for ASR only (no trans_lang here), then explicitly call plugin.translate
|
116 |
+
manager.start({
|
117 |
+
lang: this.data.languages[sourceLang].code,
|
118 |
+
// No trans_lang here, as we will call plugin.translate explicitly
|
119 |
+
});
|
120 |
+
} else {
|
121 |
+
// Use plugin for ASR only, then HF for translation
|
122 |
+
manager.start({
|
123 |
+
lang: this.data.languages[sourceLang].code,
|
124 |
+
// No trans_lang here, as HF will handle translation
|
125 |
+
});
|
126 |
+
}
|
127 |
+
},
|
128 |
+
|
129 |
+
stopRecording: function () {
|
130 |
+
manager.stop();
|
131 |
+
},
|
132 |
+
|
133 |
+
// --- CRITICAL: Re-introduced translate function for plugin mode ---
|
134 |
+
translate: function (text) {
|
135 |
+
const { sourceLang, targetLang } = this.data;
|
136 |
+
if (sourceLang === targetLang) {
|
137 |
+
this.setData({ outputText: text });
|
138 |
+
return;
|
139 |
+
}
|
140 |
+
|
141 |
+
this.setData({ outputText: '正在翻译...' });
|
142 |
+
|
143 |
+
plugin.translate({
|
144 |
+
lfrom: this.data.languages[sourceLang].code,
|
145 |
+
lto: this.data.languages[targetLang].code,
|
146 |
+
content: text,
|
147 |
+
success: (res) => {
|
148 |
+
if (res.retcode === 0) {
|
149 |
+
this.setData({ outputText: res.result });
|
150 |
+
} else {
|
151 |
+
console.error('翻译失败', res);
|
152 |
+
this.setData({ outputText: '翻译失败' });
|
153 |
+
}
|
154 |
+
},
|
155 |
+
fail: (err) => {
|
156 |
+
console.error('翻译接口调用失败', err);
|
157 |
+
this.setData({ outputText: '翻译出错' });
|
158 |
+
}
|
159 |
+
});
|
160 |
+
},
|
161 |
+
|
162 |
+
// --- HF Bridge Translation Flow (for non-CN/EN pairs) ---
|
163 |
+
uploadAudioForASR: function (filePath) {
|
164 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
165 |
+
const fileSystemManager = wx.getFileSystemManager();
|
166 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
167 |
+
wx.request({
|
168 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
169 |
+
method: 'POST',
|
170 |
+
header: { 'Content-Type': 'application/json' },
|
171 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
172 |
+
timeout: 60000,
|
173 |
+
success: (asrRes) => {
|
174 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
175 |
+
const transcript = asrRes.data.transcript;
|
176 |
+
this.setData({ transcript });
|
177 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
178 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
179 |
+
},
|
180 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
181 |
+
});
|
182 |
+
}});
|
183 |
+
},
|
184 |
+
|
185 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
186 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
187 |
+
// Step 1: Translate source (e.g., JA) to English via HF
|
188 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
189 |
+
if (englishResult) {
|
190 |
+
// Step 2: Translate English result to final target (e.g., ZH) via HF
|
191 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
192 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
193 |
+
if (finalResult) {
|
194 |
+
this.setData({ outputText: finalResult });
|
195 |
+
}
|
196 |
+
});
|
197 |
+
}
|
198 |
+
});
|
199 |
+
},
|
200 |
+
|
201 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
202 |
+
wx.request({
|
203 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
204 |
+
method: 'POST',
|
205 |
+
header: { 'Content-Type': 'application/json' },
|
206 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
207 |
+
timeout: 30000,
|
208 |
+
success: (res) => {
|
209 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
210 |
+
callback(res.data.translated_text);
|
211 |
+
} else {
|
212 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
213 |
+
callback(null);
|
214 |
+
}
|
215 |
+
},
|
216 |
+
fail: () => {
|
217 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
218 |
+
callback(null);
|
219 |
+
}
|
220 |
+
});
|
221 |
+
}
|
222 |
+
});
|
pages/index/index_v18_old.js
ADDED
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const manager = plugin.getRecordRecognitionManager();
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
9 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
10 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
11 |
+
},
|
12 |
+
langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic
|
13 |
+
sourceLang: 'zh',
|
14 |
+
targetLang: 'en',
|
15 |
+
transcript: '',
|
16 |
+
outputText: '',
|
17 |
+
isRecording: false,
|
18 |
+
sourceLanguages: [],
|
19 |
+
targetLanguages: [],
|
20 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.initRecordManager();
|
26 |
+
},
|
27 |
+
|
28 |
+
// --- Language Selection Logic ---
|
29 |
+
initializeLanguages: function () {
|
30 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
31 |
+
this.setData({
|
32 |
+
sourceLanguages: langCodes.map(c => ({
|
33 |
+
langCode: c,
|
34 |
+
name: languages[c].name,
|
35 |
+
flag: languages[c].flag,
|
36 |
+
selected: c === sourceLang
|
37 |
+
})),
|
38 |
+
targetLanguages: langCodes.map(c => ({
|
39 |
+
langCode: c,
|
40 |
+
name: languages[c].name,
|
41 |
+
flag: languages[c].flag,
|
42 |
+
selected: c === targetLang
|
43 |
+
}))
|
44 |
+
});
|
45 |
+
},
|
46 |
+
selectSourceLanguage: function (e) {
|
47 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
48 |
+
this.setData({ sourceLang: newSourceLang }, this.initializeLanguages);
|
49 |
+
},
|
50 |
+
selectTargetLanguage: function (e) {
|
51 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
52 |
+
this.setData({ targetLang: newTargetLang }, this.initializeLanguages);
|
53 |
+
},
|
54 |
+
swapLanguages: function () {
|
55 |
+
const { sourceLang, targetLang } = this.data;
|
56 |
+
this.setData({
|
57 |
+
sourceLang: targetLang,
|
58 |
+
targetLang: sourceLang,
|
59 |
+
transcript: this.data.outputText,
|
60 |
+
outputText: this.data.transcript
|
61 |
+
}, this.initializeLanguages);
|
62 |
+
},
|
63 |
+
|
64 |
+
// --- Recorder Manager Initialization (Hybrid Mode) ---
|
65 |
+
initRecordManager: function () {
|
66 |
+
manager.onStart = () => {
|
67 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
68 |
+
};
|
69 |
+
|
70 |
+
manager.onRecognize = (res) => {
|
71 |
+
// For plugin mode, this updates transcript in real-time
|
72 |
+
// For HF mode, we only care about the final result in onStop
|
73 |
+
const { sourceLang, targetLang } = this.data;
|
74 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
75 |
+
if (isChineseEnglish) {
|
76 |
+
this.setData({ transcript: res.result });
|
77 |
+
}
|
78 |
+
};
|
79 |
+
|
80 |
+
manager.onStop = (res) => {
|
81 |
+
this.setData({ isRecording: false });
|
82 |
+
const { sourceLang, targetLang } = this.data;
|
83 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
84 |
+
|
85 |
+
if (isChineseEnglish) {
|
86 |
+
// Mode 1: Plugin handles ASR, then we explicitly call plugin.translate
|
87 |
+
if (res.result) {
|
88 |
+
this.setData({ transcript: res.result });
|
89 |
+
this.translate(res.result); // CRITICAL: Explicitly call translate
|
90 |
+
} else {
|
91 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
92 |
+
}
|
93 |
+
} else {
|
94 |
+
// Mode 2: Plugin handles ASR, then HF handles translation
|
95 |
+
if (res.tempFilePath) {
|
96 |
+
this.uploadAudioForASR(res.tempFilePath);
|
97 |
+
} else {
|
98 |
+
this.setData({ transcript: '录音文件获取失败' });
|
99 |
+
}
|
100 |
+
}
|
101 |
+
};
|
102 |
+
|
103 |
+
manager.onError = (res) => {
|
104 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
105 |
+
};
|
106 |
+
},
|
107 |
+
|
108 |
+
startRecording: function () {
|
109 |
+
const { sourceLang, targetLang } = this.data;
|
110 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
111 |
+
|
112 |
+
this.setData({ isRecording: true });
|
113 |
+
|
114 |
+
if (isChineseEnglish) {
|
115 |
+
// Use plugin for ASR only (no trans_lang here), then explicitly call plugin.translate
|
116 |
+
manager.start({
|
117 |
+
lang: this.data.languages[sourceLang].code,
|
118 |
+
// No trans_lang here, as we will call plugin.translate explicitly
|
119 |
+
});
|
120 |
+
} else {
|
121 |
+
// CRITICAL: Check if source language is supported by plugin for ASR
|
122 |
+
if (sourceLang !== 'zh' && sourceLang !== 'en') {
|
123 |
+
wx.showToast({
|
124 |
+
title: '当前源语言不支持插件ASR',
|
125 |
+
icon: 'none'
|
126 |
+
});
|
127 |
+
this.setData({ isRecording: false, transcript: '请选择中文或英文作为源语言' });
|
128 |
+
return;
|
129 |
+
}
|
130 |
+
// Use plugin for ASR only, then HF for translation
|
131 |
+
manager.start({
|
132 |
+
lang: this.data.languages[sourceLang].code,
|
133 |
+
// No trans_lang here, as HF will handle translation
|
134 |
+
});
|
135 |
+
}
|
136 |
+
},
|
137 |
+
|
138 |
+
stopRecording: function () {
|
139 |
+
manager.stop();
|
140 |
+
},
|
141 |
+
|
142 |
+
// --- CRITICAL: Re-introduced translate function for plugin mode ---
|
143 |
+
translate: function (text) {
|
144 |
+
const { sourceLang, targetLang } = this.data;
|
145 |
+
if (sourceLang === targetLang) {
|
146 |
+
this.setData({ outputText: text });
|
147 |
+
return;
|
148 |
+
}
|
149 |
+
|
150 |
+
this.setData({ outputText: '正在翻译...' });
|
151 |
+
|
152 |
+
plugin.translate({
|
153 |
+
lfrom: this.data.languages[sourceLang].code,
|
154 |
+
lto: this.data.languages[targetLang].code,
|
155 |
+
content: text,
|
156 |
+
success: (res) => {
|
157 |
+
if (res.retcode === 0) {
|
158 |
+
this.setData({ outputText: res.result });
|
159 |
+
} else {
|
160 |
+
console.error('翻译失败', res);
|
161 |
+
this.setData({ outputText: '翻译失败' });
|
162 |
+
}
|
163 |
+
},
|
164 |
+
fail: (err) => {
|
165 |
+
console.error('翻译接口调用失败', err);
|
166 |
+
this.setData({ outputText: '翻译出错' });
|
167 |
+
}
|
168 |
+
});
|
169 |
+
},
|
170 |
+
|
171 |
+
// --- HF Bridge Translation Flow (for non-CN/EN pairs) ---
|
172 |
+
uploadAudioForASR: function (filePath) {
|
173 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
174 |
+
const fileSystemManager = wx.getFileSystemManager();
|
175 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
176 |
+
wx.request({
|
177 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
178 |
+
method: 'POST',
|
179 |
+
header: { 'Content-Type': 'application/json' },
|
180 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
181 |
+
timeout: 60000,
|
182 |
+
success: (asrRes) => {
|
183 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
184 |
+
const transcript = asrRes.data.transcript;
|
185 |
+
this.setData({ transcript });
|
186 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
187 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
188 |
+
},
|
189 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
190 |
+
});
|
191 |
+
}});
|
192 |
+
},
|
193 |
+
|
194 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
195 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
196 |
+
// Step 1: Translate source (e.g., JA) to English via HF
|
197 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
198 |
+
if (englishResult) {
|
199 |
+
// Step 2: Translate English result to final target (e.g., ZH) via HF
|
200 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
201 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
202 |
+
if (finalResult) {
|
203 |
+
this.setData({ outputText: finalResult });
|
204 |
+
}
|
205 |
+
});
|
206 |
+
}
|
207 |
+
});
|
208 |
+
},
|
209 |
+
|
210 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
211 |
+
wx.request({
|
212 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
213 |
+
method: 'POST',
|
214 |
+
header: { 'Content-Type': 'application/json' },
|
215 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
216 |
+
timeout: 30000,
|
217 |
+
success: (res) => {
|
218 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
219 |
+
callback(res.data.translated_text);
|
220 |
+
} else {
|
221 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
222 |
+
callback(null);
|
223 |
+
}
|
224 |
+
},
|
225 |
+
fail: () => {
|
226 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
227 |
+
callback(null);
|
228 |
+
}
|
229 |
+
});
|
230 |
+
}
|
231 |
+
});
|
pages/index/index_v19_old.js
ADDED
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const pluginManager = plugin.getRecordRecognitionManager(); // For CN/EN ASR & Translation
|
3 |
+
const nativeRecorderManager = wx.getRecorderManager(); // For JA/KO ASR (recording only)
|
4 |
+
|
5 |
+
Page({
|
6 |
+
data: {
|
7 |
+
languages: {
|
8 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
9 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
10 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
11 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
12 |
+
},
|
13 |
+
langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic
|
14 |
+
sourceLang: 'zh',
|
15 |
+
targetLang: 'en',
|
16 |
+
transcript: '',
|
17 |
+
outputText: '',
|
18 |
+
isRecording: false,
|
19 |
+
sourceLanguages: [],
|
20 |
+
targetLanguages: [],
|
21 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
22 |
+
currentActiveManager: null // To track which manager is active
|
23 |
+
},
|
24 |
+
|
25 |
+
onLoad: function () {
|
26 |
+
this.initializeLanguages();
|
27 |
+
this.initPluginManager();
|
28 |
+
this.initNativeRecorderManager();
|
29 |
+
},
|
30 |
+
|
31 |
+
// --- Language Selection Logic ---
|
32 |
+
initializeLanguages: function () {
|
33 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
34 |
+
this.setData({
|
35 |
+
sourceLanguages: langCodes.map(c => ({
|
36 |
+
langCode: c,
|
37 |
+
name: languages[c].name,
|
38 |
+
flag: languages[c].flag,
|
39 |
+
selected: c === sourceLang
|
40 |
+
})),
|
41 |
+
targetLanguages: langCodes.map(c => ({
|
42 |
+
langCode: c,
|
43 |
+
name: languages[c].name,
|
44 |
+
flag: languages[c].flag,
|
45 |
+
selected: c === targetLang
|
46 |
+
}))
|
47 |
+
});
|
48 |
+
},
|
49 |
+
selectSourceLanguage: function (e) {
|
50 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
51 |
+
this.setData({ sourceLang: newSourceLang }, this.initializeLanguages);
|
52 |
+
},
|
53 |
+
selectTargetLanguage: function (e) {
|
54 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
55 |
+
this.setData({ targetLang: newTargetLang }, this.initializeLanguages);
|
56 |
+
},
|
57 |
+
swapLanguages: function () {
|
58 |
+
const { sourceLang, targetLang } = this.data;
|
59 |
+
this.setData({
|
60 |
+
sourceLang: targetLang,
|
61 |
+
targetLang: sourceLang,
|
62 |
+
transcript: this.data.outputText,
|
63 |
+
outputText: this.data.transcript
|
64 |
+
}, this.initializeLanguages);
|
65 |
+
},
|
66 |
+
|
67 |
+
// --- Plugin Manager Initialization (for CN/EN) ---
|
68 |
+
initPluginManager: function () {
|
69 |
+
pluginManager.onStart = () => {
|
70 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
71 |
+
};
|
72 |
+
|
73 |
+
pluginManager.onRecognize = (res) => {
|
74 |
+
this.setData({ transcript: res.result });
|
75 |
+
};
|
76 |
+
|
77 |
+
pluginManager.onStop = (res) => {
|
78 |
+
this.setData({ isRecording: false });
|
79 |
+
if (res.result) {
|
80 |
+
this.setData({ transcript: res.result });
|
81 |
+
this.translate(res.result); // Explicitly call translate for plugin mode
|
82 |
+
} else {
|
83 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
84 |
+
}
|
85 |
+
};
|
86 |
+
|
87 |
+
pluginManager.onError = (res) => {
|
88 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
89 |
+
};
|
90 |
+
},
|
91 |
+
|
92 |
+
// --- Native Recorder Manager Initialization (for JA/KO recording) ---
|
93 |
+
initNativeRecorderManager: function () {
|
94 |
+
nativeRecorderManager.onStart = () => {
|
95 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
96 |
+
};
|
97 |
+
|
98 |
+
nativeRecorderManager.onStop = (res) => {
|
99 |
+
this.setData({ isRecording: false });
|
100 |
+
if (res.tempFilePath) {
|
101 |
+
this.uploadAudioForASR(res.tempFilePath);
|
102 |
+
} else {
|
103 |
+
this.setData({ transcript: '录音文件获取失败' });
|
104 |
+
}
|
105 |
+
};
|
106 |
+
|
107 |
+
nativeRecorderManager.onError = (res) => {
|
108 |
+
this.setData({ isRecording: false, transcript: '录音失败', outputText: res.errMsg });
|
109 |
+
};
|
110 |
+
},
|
111 |
+
|
112 |
+
// --- Unified Start Recording ---
|
113 |
+
startRecording: function () {
|
114 |
+
const { sourceLang, targetLang } = this.data;
|
115 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
116 |
+
|
117 |
+
this.setData({ isRecording: true });
|
118 |
+
|
119 |
+
if (isChineseEnglish) {
|
120 |
+
this.setData({ currentActiveManager: 'plugin' });
|
121 |
+
pluginManager.start({
|
122 |
+
lang: this.data.languages[sourceLang].code,
|
123 |
+
trans_lang: this.data.languages[targetLang].code,
|
124 |
+
});
|
125 |
+
} else {
|
126 |
+
this.setData({ currentActiveManager: 'native' });
|
127 |
+
nativeRecorderManager.start({
|
128 |
+
format: 'mp3', // Ensure format is compatible with HF backend
|
129 |
+
sampleRate: 16000,
|
130 |
+
numberOfChannels: 1,
|
131 |
+
encodeBitRate: 96000,
|
132 |
+
});
|
133 |
+
}
|
134 |
+
},
|
135 |
+
|
136 |
+
// --- Unified Stop Recording ---
|
137 |
+
stopRecording: function () {
|
138 |
+
if (this.data.currentActiveManager === 'plugin') {
|
139 |
+
pluginManager.stop();
|
140 |
+
} else if (this.data.currentActiveManager === 'native') {
|
141 |
+
nativeRecorderManager.stop();
|
142 |
+
}
|
143 |
+
},
|
144 |
+
|
145 |
+
// --- Plugin Translation Function (for CN/EN) ---
|
146 |
+
translate: function (text) {
|
147 |
+
const { sourceLang, targetLang } = this.data;
|
148 |
+
if (sourceLang === targetLang) {
|
149 |
+
this.setData({ outputText: text });
|
150 |
+
return;
|
151 |
+
}
|
152 |
+
|
153 |
+
this.setData({ outputText: '正在翻译...' });
|
154 |
+
|
155 |
+
plugin.translate({
|
156 |
+
lfrom: this.data.languages[sourceLang].code,
|
157 |
+
lto: this.data.languages[targetLang].code,
|
158 |
+
content: text,
|
159 |
+
success: (res) => {
|
160 |
+
if (res.retcode === 0) {
|
161 |
+
this.setData({ outputText: res.result });
|
162 |
+
} else {
|
163 |
+
console.error('翻译失败', res);
|
164 |
+
this.setData({ outputText: '翻译失败' });
|
165 |
+
}
|
166 |
+
},
|
167 |
+
fail: (err) => {
|
168 |
+
console.error('翻译接口调用失败', err);
|
169 |
+
this.setData({ outputText: '翻译出错' });
|
170 |
+
}
|
171 |
+
});
|
172 |
+
},
|
173 |
+
|
174 |
+
// --- HF Bridge Translation Flow (for non-CN/EN pairs) ---
|
175 |
+
uploadAudioForASR: function (filePath) {
|
176 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
177 |
+
const fileSystemManager = wx.getFileSystemManager();
|
178 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
179 |
+
wx.request({
|
180 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
181 |
+
method: 'POST',
|
182 |
+
header: { 'Content-Type': 'application/json' },
|
183 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
184 |
+
timeout: 60000,
|
185 |
+
success: (asrRes) => {
|
186 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
187 |
+
const transcript = asrRes.data.transcript;
|
188 |
+
this.setData({ transcript });
|
189 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
190 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
191 |
+
},
|
192 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
193 |
+
});
|
194 |
+
}});
|
195 |
+
},
|
196 |
+
|
197 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
198 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
199 |
+
// Step 1: Translate source (e.g., JA) to English via HF
|
200 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
201 |
+
if (englishResult) {
|
202 |
+
// Step 2: Translate English result to final target (e.g., ZH) via HF
|
203 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
204 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
205 |
+
if (finalResult) {
|
206 |
+
this.setData({ outputText: finalResult });
|
207 |
+
}
|
208 |
+
});
|
209 |
+
}
|
210 |
+
});
|
211 |
+
},
|
212 |
+
|
213 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
214 |
+
wx.request({
|
215 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
216 |
+
method: 'POST',
|
217 |
+
header: { 'Content-Type': 'application/json' },
|
218 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
219 |
+
timeout: 30000,
|
220 |
+
success: (res) => {
|
221 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
222 |
+
callback(res.data.translated_text);
|
223 |
+
} else {
|
224 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
225 |
+
callback(null);
|
226 |
+
}
|
227 |
+
},
|
228 |
+
fail: () => {
|
229 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
230 |
+
callback(null);
|
231 |
+
}
|
232 |
+
});
|
233 |
+
}
|
234 |
+
});
|
pages/index/index_v20_old.js
ADDED
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const manager = plugin.getRecordRecognitionManager(); // Only one manager
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
9 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
10 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
11 |
+
},
|
12 |
+
langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic
|
13 |
+
sourceLang: 'zh',
|
14 |
+
targetLang: 'en',
|
15 |
+
transcript: '',
|
16 |
+
outputText: '',
|
17 |
+
isRecording: false,
|
18 |
+
sourceLanguages: [],
|
19 |
+
targetLanguages: [],
|
20 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.initRecordManager();
|
26 |
+
},
|
27 |
+
|
28 |
+
// --- Language Selection Logic ---
|
29 |
+
initializeLanguages: function () {
|
30 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
31 |
+
this.setData({
|
32 |
+
sourceLanguages: langCodes.map(c => ({
|
33 |
+
langCode: c,
|
34 |
+
name: languages[c].name,
|
35 |
+
flag: languages[c].flag,
|
36 |
+
selected: c === sourceLang
|
37 |
+
})),
|
38 |
+
targetLanguages: langCodes.map(c => ({
|
39 |
+
langCode: c,
|
40 |
+
name: languages[c].name,
|
41 |
+
flag: languages[c].flag,
|
42 |
+
selected: c === targetLang
|
43 |
+
}))
|
44 |
+
});
|
45 |
+
},
|
46 |
+
selectSourceLanguage: function (e) {
|
47 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
48 |
+
this.setData({ sourceLang: newSourceLang }, this.initializeLanguages);
|
49 |
+
},
|
50 |
+
selectTargetLanguage: function (e) {
|
51 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
52 |
+
this.setData({ targetLang: newTargetLang }, this.initializeLanguages);
|
53 |
+
},
|
54 |
+
swapLanguages: function () {
|
55 |
+
const { sourceLang, targetLang } = this.data;
|
56 |
+
this.setData({
|
57 |
+
sourceLang: targetLang,
|
58 |
+
targetLang: sourceLang,
|
59 |
+
transcript: this.data.outputText,
|
60 |
+
outputText: this.data.transcript
|
61 |
+
}, this.initializeLanguages);
|
62 |
+
},
|
63 |
+
|
64 |
+
// --- Recorder Manager Initialization (Single Manager Mode) ---
|
65 |
+
initRecordManager: function () {
|
66 |
+
manager.onStart = () => {
|
67 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
68 |
+
};
|
69 |
+
|
70 |
+
manager.onRecognize = (res) => {
|
71 |
+
// Only update transcript for plugin-handled ASR (CN/EN)
|
72 |
+
const { sourceLang } = this.data;
|
73 |
+
if (sourceLang === 'zh' || sourceLang === 'en') {
|
74 |
+
this.setData({ transcript: res.result });
|
75 |
+
}
|
76 |
+
};
|
77 |
+
|
78 |
+
manager.onStop = (res) => {
|
79 |
+
this.setData({ isRecording: false });
|
80 |
+
const { sourceLang, targetLang } = this.data;
|
81 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
82 |
+
|
83 |
+
if (isChineseEnglish) {
|
84 |
+
// Mode 1: Plugin handles both ASR and Translation
|
85 |
+
if (res.result) {
|
86 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
87 |
+
} else {
|
88 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
89 |
+
}
|
90 |
+
} else {
|
91 |
+
// Mode 2: Plugin handles ASR (if CN/EN), then HF handles translation
|
92 |
+
if (res.result) { // res.result is the ASR result from plugin
|
93 |
+
this.setData({ transcript: res.result });
|
94 |
+
this.fullBackendBridge(res.result, this.data.sourceLang, this.data.targetLang);
|
95 |
+
} else {
|
96 |
+
this.setData({ transcript: '识别结果为空' });
|
97 |
+
}
|
98 |
+
}
|
99 |
+
};
|
100 |
+
|
101 |
+
manager.onError = (res) => {
|
102 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
103 |
+
};
|
104 |
+
},
|
105 |
+
|
106 |
+
startRecording: function () {
|
107 |
+
const { sourceLang, targetLang } = this.data;
|
108 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
109 |
+
|
110 |
+
this.setData({ isRecording: true });
|
111 |
+
|
112 |
+
// CRITICAL: Check if source language is supported by plugin for ASR
|
113 |
+
if (sourceLang !== 'zh' && sourceLang !== 'en') {
|
114 |
+
wx.showToast({
|
115 |
+
title: '当前源语言不支持语音输入',
|
116 |
+
icon: 'none'
|
117 |
+
});
|
118 |
+
this.setData({ isRecording: false, transcript: '请选择中文或英文作为源语言' });
|
119 |
+
return;
|
120 |
+
}
|
121 |
+
|
122 |
+
if (isChineseEnglish) {
|
123 |
+
// Use plugin for ASR and Translation
|
124 |
+
manager.start({
|
125 |
+
lang: this.data.languages[sourceLang].code,
|
126 |
+
trans_lang: this.data.languages[targetLang].code,
|
127 |
+
});
|
128 |
+
} else {
|
129 |
+
// Use plugin for ASR only, then HF for translation
|
130 |
+
manager.start({
|
131 |
+
lang: this.data.languages[sourceLang].code,
|
132 |
+
// No trans_lang here, as HF will handle translation
|
133 |
+
});
|
134 |
+
}
|
135 |
+
},
|
136 |
+
|
137 |
+
stopRecording: function () {
|
138 |
+
manager.stop();
|
139 |
+
},
|
140 |
+
|
141 |
+
// --- HF Bridge Translation Flow (for non-CN/EN pairs) ---
|
142 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
143 |
+
this.setData({ outputText: '翻译中 (1/2)..' }); // Simplified steps
|
144 |
+
// Step 1: Translate source (e.g., ZH) to English via HF
|
145 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
146 |
+
if (englishResult) {
|
147 |
+
// Step 2: Translate English result to final target (e.g., JA) via HF
|
148 |
+
this.setData({ outputText: '翻译中 (2/2)..' });
|
149 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
150 |
+
if (finalResult) {
|
151 |
+
this.setData({ outputText: finalResult });
|
152 |
+
}
|
153 |
+
});
|
154 |
+
}
|
155 |
+
});
|
156 |
+
},
|
157 |
+
|
158 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
159 |
+
wx.request({
|
160 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
161 |
+
method: 'POST',
|
162 |
+
header: { 'Content-Type': 'application/json' },
|
163 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
164 |
+
timeout: 30000,
|
165 |
+
success: (res) => {
|
166 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
167 |
+
callback(res.data.translated_text);
|
168 |
+
} else {
|
169 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
170 |
+
callback(null);
|
171 |
+
}
|
172 |
+
},
|
173 |
+
fail: () => {
|
174 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
175 |
+
callback(null);
|
176 |
+
}
|
177 |
+
});
|
178 |
+
}
|
179 |
+
});
|
pages/index/index_v21_old.js
ADDED
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const manager = plugin.getRecordRecognitionManager();
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
9 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
10 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
11 |
+
},
|
12 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
13 |
+
sourceLang: 'zh',
|
14 |
+
targetLang: 'en',
|
15 |
+
transcript: '',
|
16 |
+
outputText: '',
|
17 |
+
isRecording: false,
|
18 |
+
sourceLanguages: [],
|
19 |
+
targetLanguages: [],
|
20 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
21 |
+
// This flag will determine if we need to call our backend
|
22 |
+
useHfBridge: false
|
23 |
+
},
|
24 |
+
|
25 |
+
onLoad: function () {
|
26 |
+
this.initializeLanguages();
|
27 |
+
this.initManager();
|
28 |
+
},
|
29 |
+
|
30 |
+
// --- Language Selection Logic ---
|
31 |
+
initializeLanguages: function () {
|
32 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
33 |
+
this.setData({
|
34 |
+
sourceLanguages: langCodes.map(c => ({
|
35 |
+
langCode: c,
|
36 |
+
name: languages[c].name,
|
37 |
+
flag: languages[c].flag,
|
38 |
+
selected: c === sourceLang
|
39 |
+
})),
|
40 |
+
targetLanguages: langCodes.map(c => ({
|
41 |
+
langCode: c,
|
42 |
+
name: languages[c].name,
|
43 |
+
flag: languages[c].flag,
|
44 |
+
selected: c === targetLang
|
45 |
+
}))
|
46 |
+
});
|
47 |
+
},
|
48 |
+
selectSourceLanguage: function (e) {
|
49 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
50 |
+
this.setData({ sourceLang: newSourceLang }, this.initializeLanguages);
|
51 |
+
},
|
52 |
+
selectTargetLanguage: function (e) {
|
53 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
54 |
+
this.setData({ targetLang: newTargetLang }, this.initializeLanguages);
|
55 |
+
},
|
56 |
+
swapLanguages: function () {
|
57 |
+
const { sourceLang, targetLang } = this.data;
|
58 |
+
this.setData({
|
59 |
+
sourceLang: targetLang,
|
60 |
+
targetLang: sourceLang,
|
61 |
+
transcript: this.data.outputText,
|
62 |
+
outputText: this.data.transcript
|
63 |
+
}, this.initializeLanguages);
|
64 |
+
},
|
65 |
+
|
66 |
+
// --- Unified Plugin Manager Initialization ---
|
67 |
+
initManager: function () {
|
68 |
+
manager.onStart = (res) => {
|
69 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
70 |
+
};
|
71 |
+
|
72 |
+
manager.onRecognize = (res) => {
|
73 |
+
// Live recognition feedback
|
74 |
+
this.setData({ transcript: res.result });
|
75 |
+
};
|
76 |
+
|
77 |
+
manager.onStop = (res) => {
|
78 |
+
this.setData({ isRecording: false });
|
79 |
+
|
80 |
+
if (res.result) {
|
81 |
+
this.setData({ transcript: res.result });
|
82 |
+
// If using HF Bridge, the plugin only provides the transcript.
|
83 |
+
// We then manually call our backend for translation.
|
84 |
+
if (this.data.useHfBridge) {
|
85 |
+
this.translateViaHfBridge(res.result);
|
86 |
+
} else {
|
87 |
+
// Otherwise, the plugin provides the translation directly.
|
88 |
+
this.setData({ outputText: res.translateResult || '翻译结果为空' });
|
89 |
+
}
|
90 |
+
} else {
|
91 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
92 |
+
}
|
93 |
+
};
|
94 |
+
|
95 |
+
manager.onError = (res) => {
|
96 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
97 |
+
};
|
98 |
+
},
|
99 |
+
|
100 |
+
// --- Unified Start/Stop Recording ---
|
101 |
+
startRecording: function () {
|
102 |
+
const { sourceLang, targetLang, languages } = this.data;
|
103 |
+
|
104 |
+
// Block recording if source is not CN or EN
|
105 |
+
if (sourceLang !== 'zh' && sourceLang !== 'en') {
|
106 |
+
wx.showToast({
|
107 |
+
title: '语音输入暂仅支持中文和英文',
|
108 |
+
icon: 'none',
|
109 |
+
duration: 2000
|
110 |
+
});
|
111 |
+
return;
|
112 |
+
}
|
113 |
+
|
114 |
+
const isChineseEnglishPair = (sourceLang === 'zh' || sourceLang === 'en') && (targetLang === 'zh' || targetLang === 'en');
|
115 |
+
const useHfBridge = !isChineseEnglishPair;
|
116 |
+
|
117 |
+
this.setData({
|
118 |
+
isRecording: true,
|
119 |
+
useHfBridge: useHfBridge,
|
120 |
+
transcript: '',
|
121 |
+
outputText: ''
|
122 |
+
});
|
123 |
+
|
124 |
+
manager.start({
|
125 |
+
lang: languages[sourceLang].code,
|
126 |
+
trans_lang: languages[targetLang].code,
|
127 |
+
// Critical: Only enable plugin's internal translation for direct CN/EN pairs
|
128 |
+
translate: !useHfBridge
|
129 |
+
});
|
130 |
+
},
|
131 |
+
|
132 |
+
stopRecording: function () {
|
133 |
+
manager.stop();
|
134 |
+
},
|
135 |
+
|
136 |
+
// --- HF Bridge Translation Flow ---
|
137 |
+
translateViaHfBridge: function(text) {
|
138 |
+
const { sourceLang, targetLang } = this.data;
|
139 |
+
this.setData({ outputText: '翻译中 (HF)...' });
|
140 |
+
|
141 |
+
// The plugin has already done the ASR (e.g., EN).
|
142 |
+
// Now we translate it to the final target (e.g., JA).
|
143 |
+
this.translateViaHF(text, sourceLang, targetLang, (finalResult) => {
|
144 |
+
if (finalResult) {
|
145 |
+
this.setData({ outputText: finalResult });
|
146 |
+
}
|
147 |
+
// Error message is set within translateViaHF
|
148 |
+
});
|
149 |
+
},
|
150 |
+
|
151 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
152 |
+
wx.request({
|
153 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
154 |
+
method: 'POST',
|
155 |
+
header: { 'Content-Type': 'application/json' },
|
156 |
+
data: {
|
157 |
+
"text": text,
|
158 |
+
"source_lang": sourceLang,
|
159 |
+
"target_lang": targetLang
|
160 |
+
},
|
161 |
+
timeout: 45000,
|
162 |
+
success: (res) => {
|
163 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
164 |
+
callback(res.data.translated_text);
|
165 |
+
} else {
|
166 |
+
const errorMsg = res.data.error || `HF翻译失败 (${sourceLang}->${targetLang})`;
|
167 |
+
this.setData({ outputText: errorMsg });
|
168 |
+
callback(null);
|
169 |
+
}
|
170 |
+
},
|
171 |
+
fail: (err) => {
|
172 |
+
this.setData({ outputText: `HF请求失败 (${sourceLang}->${targetLang})` });
|
173 |
+
callback(null);
|
174 |
+
}
|
175 |
+
});
|
176 |
+
}
|
177 |
+
});
|
pages/index/index_v22_old.js
ADDED
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const pluginManager = plugin.getRecordRecognitionManager(); // For CN/EN ASR
|
3 |
+
const nativeRecorderManager = wx.getRecorderManager(); // For JA/KO Recording
|
4 |
+
|
5 |
+
Page({
|
6 |
+
data: {
|
7 |
+
languages: {
|
8 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN', native: false },
|
9 |
+
'en': { name: 'English', flag: 'us', code: 'en_US', native: false },
|
10 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', native: true },
|
11 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', native: true }
|
12 |
+
},
|
13 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
14 |
+
sourceLang: 'zh',
|
15 |
+
targetLang: 'en',
|
16 |
+
transcript: '',
|
17 |
+
outputText: '',
|
18 |
+
isRecording: false,
|
19 |
+
sourceLanguages: [],
|
20 |
+
targetLanguages: [],
|
21 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
22 |
+
currentManager: null // 'plugin' or 'native'
|
23 |
+
},
|
24 |
+
|
25 |
+
onLoad: function () {
|
26 |
+
this.initializeLanguages();
|
27 |
+
this.initPluginManager();
|
28 |
+
this.initNativeRecorderManager();
|
29 |
+
},
|
30 |
+
|
31 |
+
// --- Language Selection Logic ---
|
32 |
+
initializeLanguages: function () {
|
33 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
34 |
+
this.setData({
|
35 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
36 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
37 |
+
});
|
38 |
+
},
|
39 |
+
selectSourceLanguage: function (e) {
|
40 |
+
this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages);
|
41 |
+
},
|
42 |
+
selectTargetLanguage: function (e) {
|
43 |
+
this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages);
|
44 |
+
},
|
45 |
+
swapLanguages: function () {
|
46 |
+
this.setData({
|
47 |
+
sourceLang: this.data.targetLang,
|
48 |
+
targetLang: this.data.sourceLang,
|
49 |
+
transcript: this.data.outputText,
|
50 |
+
outputText: this.data.transcript
|
51 |
+
}, this.initializeLanguages);
|
52 |
+
},
|
53 |
+
|
54 |
+
// --- Manager Initializations ---
|
55 |
+
initPluginManager: function () {
|
56 |
+
pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' });
|
57 |
+
pluginManager.onRecognize = (res) => this.setData({ transcript: res.result });
|
58 |
+
pluginManager.onStop = (res) => {
|
59 |
+
this.setData({ isRecording: false });
|
60 |
+
if (!res.result) {
|
61 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
62 |
+
return;
|
63 |
+
}
|
64 |
+
this.setData({ transcript: res.result });
|
65 |
+
const { sourceLang, targetLang } = this.data;
|
66 |
+
const isChineseEnglishPair = (sourceLang === 'zh' || sourceLang === 'en') && (targetLang === 'zh' || targetLang === 'en');
|
67 |
+
|
68 |
+
if (isChineseEnglishPair) {
|
69 |
+
this.translateViaPlugin(res.result);
|
70 |
+
} else {
|
71 |
+
this.translateViaHfBridge(res.result, sourceLang, targetLang);
|
72 |
+
}
|
73 |
+
};
|
74 |
+
pluginManager.onError = (res) => this.setData({ isRecording: false, transcript: '插件识别失败', outputText: res.msg });
|
75 |
+
},
|
76 |
+
|
77 |
+
initNativeRecorderManager: function () {
|
78 |
+
nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' });
|
79 |
+
nativeRecorderManager.onStop = (res) => {
|
80 |
+
this.setData({ isRecording: false });
|
81 |
+
if (res.tempFilePath) {
|
82 |
+
this.uploadAudioForASR(res.tempFilePath);
|
83 |
+
} else {
|
84 |
+
this.setData({ transcript: '录音文件获取失败' });
|
85 |
+
}
|
86 |
+
};
|
87 |
+
nativeRecorderManager.onError = (res) => this.setData({ isRecording: false, transcript: '原生录音失败', outputText: res.errMsg });
|
88 |
+
},
|
89 |
+
|
90 |
+
// --- Recording Logic ---
|
91 |
+
startRecording: function () {
|
92 |
+
const { sourceLang, languages } = this.data;
|
93 |
+
const shouldUseNative = languages[sourceLang].native;
|
94 |
+
|
95 |
+
this.setData({ isRecording: true, currentManager: shouldUseNative ? 'native' : 'plugin' });
|
96 |
+
|
97 |
+
if (shouldUseNative) {
|
98 |
+
nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 });
|
99 |
+
} else {
|
100 |
+
pluginManager.start({ lang: languages[sourceLang].code, translate: false });
|
101 |
+
}
|
102 |
+
},
|
103 |
+
|
104 |
+
stopRecording: function () {
|
105 |
+
if (this.data.currentManager === 'native') {
|
106 |
+
nativeRecorderManager.stop();
|
107 |
+
} else {
|
108 |
+
pluginManager.stop();
|
109 |
+
}
|
110 |
+
},
|
111 |
+
|
112 |
+
// --- Translation Flows ---
|
113 |
+
translateViaPlugin: function(text) {
|
114 |
+
const { sourceLang, targetLang, languages } = this.data;
|
115 |
+
if (sourceLang === targetLang) {
|
116 |
+
this.setData({ outputText: text });
|
117 |
+
return;
|
118 |
+
}
|
119 |
+
this.setData({ outputText: '翻译中 (插件)...' });
|
120 |
+
plugin.translate({
|
121 |
+
lfrom: languages[sourceLang].code,
|
122 |
+
lto: languages[targetLang].code,
|
123 |
+
content: text,
|
124 |
+
success: (res) => {
|
125 |
+
if (res.retcode === 0) {
|
126 |
+
this.setData({ outputText: res.result });
|
127 |
+
} else {
|
128 |
+
this.setData({ outputText: '插件翻译失败' });
|
129 |
+
}
|
130 |
+
},
|
131 |
+
fail: () => this.setData({ outputText: '插件翻译接口调用失败' })
|
132 |
+
});
|
133 |
+
},
|
134 |
+
|
135 |
+
uploadAudioForASR: function (filePath) {
|
136 |
+
this.setData({ transcript: '正在识别 (HF)...' });
|
137 |
+
wx.getFileSystemManager().readFile({
|
138 |
+
filePath,
|
139 |
+
encoding: 'base64',
|
140 |
+
success: (res) => {
|
141 |
+
wx.request({
|
142 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
143 |
+
method: 'POST',
|
144 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
145 |
+
timeout: 60000,
|
146 |
+
success: (asrRes) => {
|
147 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
148 |
+
const transcript = asrRes.data.transcript;
|
149 |
+
this.setData({ transcript });
|
150 |
+
this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
151 |
+
} else {
|
152 |
+
this.setData({ transcript: 'HF识别失败', outputText: asrRes.data.error || '' });
|
153 |
+
}
|
154 |
+
},
|
155 |
+
fail: () => this.setData({ transcript: 'HF识别请求失败' })
|
156 |
+
});
|
157 |
+
},
|
158 |
+
fail: () => this.setData({ transcript: '读取录音文件失败' })
|
159 |
+
});
|
160 |
+
},
|
161 |
+
|
162 |
+
translateViaHfBridge: function(text, source, target) {
|
163 |
+
this.setData({ outputText: '翻译中 (HF)...' });
|
164 |
+
if (source === target) {
|
165 |
+
this.setData({ outputText: text });
|
166 |
+
return;
|
167 |
+
}
|
168 |
+
this.translateViaHF(text, source, target, (result) => {
|
169 |
+
if (result) {
|
170 |
+
this.setData({ outputText: result });
|
171 |
+
}
|
172 |
+
// Error message is set within translateViaHF
|
173 |
+
});
|
174 |
+
},
|
175 |
+
|
176 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
177 |
+
wx.request({
|
178 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
179 |
+
method: 'POST',
|
180 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
181 |
+
timeout: 45000,
|
182 |
+
success: (res) => {
|
183 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
184 |
+
callback(res.data.translated_text);
|
185 |
+
} else {
|
186 |
+
const errorMsg = res.data.error || `HF翻译失败`;
|
187 |
+
this.setData({ outputText: errorMsg });
|
188 |
+
callback(null);
|
189 |
+
}
|
190 |
+
},
|
191 |
+
fail: () => {
|
192 |
+
this.setData({ outputText: `HF翻译请求失败` });
|
193 |
+
callback(null);
|
194 |
+
}
|
195 |
+
});
|
196 |
+
}
|
197 |
+
});
|
pages/index/index_v24_old.js
ADDED
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const pluginManager = plugin.getRecordRecognitionManager();
|
3 |
+
const nativeRecorderManager = wx.getRecorderManager();
|
4 |
+
|
5 |
+
// Helper function to show detailed errors
|
6 |
+
function showDetailedError(title, content) {
|
7 |
+
wx.showModal({
|
8 |
+
title: title,
|
9 |
+
content: typeof content === 'object' ? JSON.stringify(content) : String(content),
|
10 |
+
showCancel: false
|
11 |
+
});
|
12 |
+
}
|
13 |
+
|
14 |
+
Page({
|
15 |
+
data: {
|
16 |
+
languages: {
|
17 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true },
|
18 |
+
'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true },
|
19 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false },
|
20 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false }
|
21 |
+
},
|
22 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
23 |
+
sourceLang: 'zh',
|
24 |
+
targetLang: 'en',
|
25 |
+
transcript: '',
|
26 |
+
outputText: '',
|
27 |
+
isRecording: false,
|
28 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
29 |
+
currentManagerType: null // 'plugin' or 'native'
|
30 |
+
},
|
31 |
+
|
32 |
+
onLoad: function () {
|
33 |
+
this.initializeLanguages();
|
34 |
+
this.initPluginManager();
|
35 |
+
this.initNativeRecorderManager();
|
36 |
+
},
|
37 |
+
|
38 |
+
// --- Language Selection Logic ---
|
39 |
+
initializeLanguages: function () {
|
40 |
+
const { languages, sourceLang, targetLang } = this.data;
|
41 |
+
this.setData({
|
42 |
+
sourceLanguages: Object.keys(languages).map(key => ({ ...languages[key], langCode: key, selected: key === sourceLang })),
|
43 |
+
targetLanguages: Object.keys(languages).map(key => ({ ...languages[key], langCode: key, selected: key === targetLang }))
|
44 |
+
});
|
45 |
+
},
|
46 |
+
selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
47 |
+
selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
48 |
+
swapLanguages: function () {
|
49 |
+
this.setData({
|
50 |
+
sourceLang: this.data.targetLang,
|
51 |
+
targetLang: this.data.sourceLang,
|
52 |
+
transcript: this.data.outputText,
|
53 |
+
outputText: this.data.transcript
|
54 |
+
}, this.initializeLanguages);
|
55 |
+
},
|
56 |
+
|
57 |
+
// --- Manager Initializations with Detailed Error Popups ---
|
58 |
+
initPluginManager: function () {
|
59 |
+
pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' });
|
60 |
+
pluginManager.onRecognize = (res) => this.setData({ transcript: res.result });
|
61 |
+
pluginManager.onStop = (res) => {
|
62 |
+
this.setData({ isRecording: false });
|
63 |
+
if (!res.result) { return this.setData({ transcript: '识别结果为空' }); }
|
64 |
+
this.setData({ transcript: res.result });
|
65 |
+
const { sourceLang, targetLang } = this.data;
|
66 |
+
const isPluginTarget = this.data.languages[targetLang].usePlugin;
|
67 |
+
if (isPluginTarget) {
|
68 |
+
this.translateViaPlugin(res.result);
|
69 |
+
} else {
|
70 |
+
this.translateViaHfBridge(res.result, sourceLang, targetLang);
|
71 |
+
}
|
72 |
+
};
|
73 |
+
pluginManager.onError = (res) => {
|
74 |
+
this.setData({ isRecording: false });
|
75 |
+
showDetailedError('插件录音失败', res);
|
76 |
+
};
|
77 |
+
},
|
78 |
+
|
79 |
+
initNativeRecorderManager: function () {
|
80 |
+
nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' });
|
81 |
+
nativeRecorderManager.onStop = (res) => {
|
82 |
+
this.setData({ isRecording: false });
|
83 |
+
if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); }
|
84 |
+
else { this.setData({ transcript: '原生录音文件获取失败' }); }
|
85 |
+
};
|
86 |
+
nativeRecorderManager.onError = (res) => {
|
87 |
+
this.setData({ isRecording: false });
|
88 |
+
showDetailedError('原生录音失败', res);
|
89 |
+
};
|
90 |
+
},
|
91 |
+
|
92 |
+
// --- Main Record Button Handler ---
|
93 |
+
handleRecordToggle: function() {
|
94 |
+
if (this.data.isRecording) {
|
95 |
+
this.stopRecording();
|
96 |
+
return;
|
97 |
+
}
|
98 |
+
wx.getSetting({
|
99 |
+
success: (res) => {
|
100 |
+
if (!res.authSetting['scope.record']) {
|
101 |
+
wx.authorize({
|
102 |
+
scope: 'scope.record',
|
103 |
+
success: this.startRecording,
|
104 |
+
fail: (err) => showDetailedError('授权失败', err)
|
105 |
+
});
|
106 |
+
} else {
|
107 |
+
this.startRecording();
|
108 |
+
}
|
109 |
+
},
|
110 |
+
fail: (err) => showDetailedError('无法获取权限设置', err)
|
111 |
+
});
|
112 |
+
},
|
113 |
+
|
114 |
+
startRecording: function () {
|
115 |
+
// *** DEBUG LOGGING ***
|
116 |
+
console.log(`[DEBUG] Attempting to start recording. Source Language: ${this.data.sourceLang}`);
|
117 |
+
|
118 |
+
const { sourceLang, languages } = this.data;
|
119 |
+
const shouldUsePlugin = languages[sourceLang].usePlugin;
|
120 |
+
const managerType = shouldUsePlugin ? 'plugin' : 'native';
|
121 |
+
|
122 |
+
this.setData({ isRecording: true, currentManagerType: managerType });
|
123 |
+
|
124 |
+
if (shouldUsePlugin) {
|
125 |
+
pluginManager.start({ lang: languages[sourceLang].code, translate: false });
|
126 |
+
} else {
|
127 |
+
nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 });
|
128 |
+
}
|
129 |
+
},
|
130 |
+
|
131 |
+
stopRecording: function () {
|
132 |
+
if (this.data.currentManagerType === 'native') {
|
133 |
+
nativeRecorderManager.stop();
|
134 |
+
} else {
|
135 |
+
pluginManager.stop();
|
136 |
+
}
|
137 |
+
},
|
138 |
+
|
139 |
+
// --- Translation Logic (unchanged) ---
|
140 |
+
translateViaPlugin: function(text) {
|
141 |
+
const { sourceLang, targetLang, languages } = this.data;
|
142 |
+
if (sourceLang === targetLang) { return this.setData({ outputText: text }); }
|
143 |
+
this.setData({ outputText: '翻译中 (插件)...' });
|
144 |
+
plugin.translate({
|
145 |
+
lfrom: languages[sourceLang].code,
|
146 |
+
lto: languages[targetLang].code,
|
147 |
+
content: text,
|
148 |
+
success: (res) => {
|
149 |
+
if (res.retcode === 0) { this.setData({ outputText: res.result }); }
|
150 |
+
else { showDetailedError('插件翻译失败', res); }
|
151 |
+
},
|
152 |
+
fail: (err) => showDetailedError('插件翻译接口调用失败', err)
|
153 |
+
});
|
154 |
+
},
|
155 |
+
|
156 |
+
uploadAudioForASR: function (filePath) {
|
157 |
+
this.setData({ transcript: '正在识别 (HF)...' });
|
158 |
+
wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => {
|
159 |
+
wx.request({
|
160 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
161 |
+
method: 'POST',
|
162 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
163 |
+
timeout: 60000,
|
164 |
+
success: (asrRes) => {
|
165 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
166 |
+
const transcript = asrRes.data.transcript;
|
167 |
+
this.setData({ transcript });
|
168 |
+
this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
169 |
+
} else { showDetailedError('HF识别失败', asrRes.data); }
|
170 |
+
},
|
171 |
+
fail: (err) => showDetailedError('HF识别请求失败', err)
|
172 |
+
});
|
173 |
+
}});
|
174 |
+
},
|
175 |
+
|
176 |
+
translateViaHfBridge: function(text, source, target) {
|
177 |
+
if (source === target) { return this.setData({ outputText: text }); }
|
178 |
+
this.setData({ outputText: '翻译中 (HF)...' });
|
179 |
+
this.translateViaHF(text, source, target, (result) => {
|
180 |
+
if (result) { this.setData({ outputText: result }); }
|
181 |
+
});
|
182 |
+
},
|
183 |
+
|
184 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
185 |
+
wx.request({
|
186 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
187 |
+
method: 'POST',
|
188 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
189 |
+
timeout: 45000,
|
190 |
+
success: (res) => {
|
191 |
+
if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); }
|
192 |
+
else { showDetailedError('HF翻译失败', res.data); callback(null); }
|
193 |
+
},
|
194 |
+
fail: (err) => { showDetailedError('HF翻译请求失败', err); callback(null); }
|
195 |
+
});
|
196 |
+
}
|
197 |
+
});
|
pages/index/index_v25_old.js
ADDED
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const pluginManager = plugin.getRecordRecognitionManager();
|
3 |
+
const nativeRecorderManager = wx.getRecorderManager();
|
4 |
+
|
5 |
+
// Helper function to show detailed errors
|
6 |
+
function showDetailedError(title, content) {
|
7 |
+
wx.showModal({
|
8 |
+
title: title,
|
9 |
+
content: typeof content === 'object' ? JSON.stringify(content) : String(content),
|
10 |
+
showCancel: false
|
11 |
+
});
|
12 |
+
}
|
13 |
+
|
14 |
+
Page({
|
15 |
+
data: {
|
16 |
+
// Static language configuration
|
17 |
+
languages: {
|
18 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true },
|
19 |
+
'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true },
|
20 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false },
|
21 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false }
|
22 |
+
},
|
23 |
+
// Dynamic data
|
24 |
+
sourceLang: 'zh',
|
25 |
+
targetLang: 'en',
|
26 |
+
transcript: '',
|
27 |
+
outputText: '',
|
28 |
+
isRecording: false,
|
29 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
30 |
+
currentManagerType: null // 'plugin' or 'native'
|
31 |
+
},
|
32 |
+
|
33 |
+
onLoad: function () {
|
34 |
+
// Set the full language arrays for the UI to use
|
35 |
+
this.setData({
|
36 |
+
sourceLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: this.getLangCode(lang.code)})),
|
37 |
+
targetLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: this.getLangCode(lang.code)}))
|
38 |
+
});
|
39 |
+
this.initPluginManager();
|
40 |
+
this.initNativeRecorderManager();
|
41 |
+
},
|
42 |
+
|
43 |
+
getLangCode: function(fullCode) {
|
44 |
+
return fullCode.split('_')[0];
|
45 |
+
},
|
46 |
+
|
47 |
+
// --- Language Selection Logic (Simplified and Synchronous) ---
|
48 |
+
selectSourceLanguage: function (e) {
|
49 |
+
this.setData({ sourceLang: e.currentTarget.dataset.langCode });
|
50 |
+
},
|
51 |
+
selectTargetLanguage: function (e) {
|
52 |
+
this.setData({ targetLang: e.currentTarget.dataset.langCode });
|
53 |
+
},
|
54 |
+
swapLanguages: function () {
|
55 |
+
const { sourceLang, targetLang } = this.data;
|
56 |
+
this.setData({
|
57 |
+
sourceLang: targetLang,
|
58 |
+
targetLang: sourceLang,
|
59 |
+
transcript: this.data.outputText,
|
60 |
+
outputText: this.data.transcript
|
61 |
+
});
|
62 |
+
},
|
63 |
+
|
64 |
+
// --- Manager Initializations ---
|
65 |
+
initPluginManager: function () {
|
66 |
+
pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' });
|
67 |
+
pluginManager.onRecognize = (res) => this.setData({ transcript: res.result });
|
68 |
+
pluginManager.onStop = (res) => {
|
69 |
+
this.setData({ isRecording: false });
|
70 |
+
if (!res.result) { return this.setData({ transcript: '识别结果为空' }); }
|
71 |
+
this.setData({ transcript: res.result });
|
72 |
+
const { sourceLang, targetLang, languages } = this.data;
|
73 |
+
const isPluginTarget = languages[targetLang].usePlugin;
|
74 |
+
if (sourceLang === targetLang) {
|
75 |
+
this.setData({ outputText: res.result });
|
76 |
+
} else if (isPluginTarget) {
|
77 |
+
this.translateViaPlugin(res.result);
|
78 |
+
} else {
|
79 |
+
this.translateViaHfBridge(res.result, sourceLang, targetLang);
|
80 |
+
}
|
81 |
+
};
|
82 |
+
pluginManager.onError = (res) => {
|
83 |
+
this.setData({ isRecording: false });
|
84 |
+
showDetailedError('插件录音失败', res);
|
85 |
+
};
|
86 |
+
},
|
87 |
+
|
88 |
+
initNativeRecorderManager: function () {
|
89 |
+
nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' });
|
90 |
+
nativeRecorderManager.onStop = (res) => {
|
91 |
+
this.setData({ isRecording: false });
|
92 |
+
if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); }
|
93 |
+
else { this.setData({ transcript: '原生录音文件获取失败' }); }
|
94 |
+
};
|
95 |
+
nativeRecorderManager.onError = (res) => {
|
96 |
+
this.setData({ isRecording: false });
|
97 |
+
showDetailedError('原生录音失败', res);
|
98 |
+
};
|
99 |
+
},
|
100 |
+
|
101 |
+
// --- Main Record Button Handler ---
|
102 |
+
handleRecordToggle: function() {
|
103 |
+
if (this.data.isRecording) {
|
104 |
+
this.stopRecording();
|
105 |
+
return;
|
106 |
+
}
|
107 |
+
wx.getSetting({
|
108 |
+
success: (res) => {
|
109 |
+
if (!res.authSetting['scope.record']) {
|
110 |
+
wx.authorize({
|
111 |
+
scope: 'scope.record',
|
112 |
+
success: this.startRecording,
|
113 |
+
fail: (err) => showDetailedError('授权失败', err)
|
114 |
+
});
|
115 |
+
} else {
|
116 |
+
this.startRecording();
|
117 |
+
}
|
118 |
+
},
|
119 |
+
fail: (err) => showDetailedError('无法获取权限设置', err)
|
120 |
+
});
|
121 |
+
},
|
122 |
+
|
123 |
+
startRecording: function () {
|
124 |
+
console.log(`[DEBUG] Starting recording. Source: ${this.data.sourceLang}, Target: ${this.data.targetLang}`);
|
125 |
+
const { sourceLang, languages } = this.data;
|
126 |
+
const shouldUsePlugin = languages[sourceLang].usePlugin;
|
127 |
+
const managerType = shouldUsePlugin ? 'plugin' : 'native';
|
128 |
+
|
129 |
+
this.setData({ isRecording: true, currentManagerType: managerType });
|
130 |
+
|
131 |
+
if (shouldUsePlugin) {
|
132 |
+
pluginManager.start({ lang: languages[sourceLang].code, translate: false });
|
133 |
+
} else {
|
134 |
+
nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 });
|
135 |
+
}
|
136 |
+
},
|
137 |
+
|
138 |
+
stopRecording: function () {
|
139 |
+
if (this.data.currentManagerType === 'native') {
|
140 |
+
nativeRecorderManager.stop();
|
141 |
+
} else {
|
142 |
+
pluginManager.stop();
|
143 |
+
}
|
144 |
+
},
|
145 |
+
|
146 |
+
// --- Translation Logic ---
|
147 |
+
translateViaPlugin: function(text) {
|
148 |
+
const { sourceLang, targetLang, languages } = this.data;
|
149 |
+
this.setData({ outputText: '翻译中 (插件)...' });
|
150 |
+
plugin.translate({
|
151 |
+
lfrom: languages[sourceLang].code,
|
152 |
+
lto: languages[targetLang].code,
|
153 |
+
content: text,
|
154 |
+
success: (res) => {
|
155 |
+
if (res.retcode === 0) { this.setData({ outputText: res.result }); }
|
156 |
+
else { showDetailedError('插件翻译失败', res); }
|
157 |
+
},
|
158 |
+
fail: (err) => showDetailedError('插件翻译接口调用失败', err)
|
159 |
+
});
|
160 |
+
},
|
161 |
+
|
162 |
+
uploadAudioForASR: function (filePath) {
|
163 |
+
this.setData({ transcript: '正在识别 (HF)...' });
|
164 |
+
wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => {
|
165 |
+
wx.request({
|
166 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
167 |
+
method: 'POST',
|
168 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
169 |
+
timeout: 60000,
|
170 |
+
success: (asrRes) => {
|
171 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
172 |
+
const transcript = asrRes.data.transcript;
|
173 |
+
this.setData({ transcript });
|
174 |
+
this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
175 |
+
} else { showDetailedError('HF识别失败', asrRes.data); }
|
176 |
+
},
|
177 |
+
fail: (err) => showDetailedError('HF识别请求失败', err)
|
178 |
+
});
|
179 |
+
}});
|
180 |
+
},
|
181 |
+
|
182 |
+
translateViaHfBridge: function(text, source, target) {
|
183 |
+
if (source === target) { return this.setData({ outputText: text }); }
|
184 |
+
this.setData({ outputText: '翻译中 (HF)...' });
|
185 |
+
this.translateViaHF(text, source, target, (result) => {
|
186 |
+
if (result) { this.setData({ outputText: result }); }
|
187 |
+
});
|
188 |
+
},
|
189 |
+
|
190 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
191 |
+
wx.request({
|
192 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
193 |
+
method: 'POST',
|
194 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
195 |
+
timeout: 45000,
|
196 |
+
success: (res) => {
|
197 |
+
if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); }
|
198 |
+
else { showDetailedError('HF翻译失败', res.data); callback(null); }
|
199 |
+
},
|
200 |
+
fail: (err) => { showDetailedError('HF翻译请求失败', err); callback(null); }
|
201 |
+
});
|
202 |
+
}
|
203 |
+
});
|
pages/index/index_v26_old.js
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const pluginManager = plugin.getRecordRecognitionManager();
|
3 |
+
const nativeRecorderManager = wx.getRecorderManager();
|
4 |
+
|
5 |
+
function showDetailedError(title, content) {
|
6 |
+
wx.showModal({
|
7 |
+
title: title,
|
8 |
+
content: typeof content === 'object' ? JSON.stringify(content) : String(content),
|
9 |
+
showCancel: false
|
10 |
+
});
|
11 |
+
}
|
12 |
+
|
13 |
+
Page({
|
14 |
+
data: {
|
15 |
+
languages: {
|
16 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true },
|
17 |
+
'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true },
|
18 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false },
|
19 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false }
|
20 |
+
},
|
21 |
+
sourceLang: 'zh',
|
22 |
+
targetLang: 'en',
|
23 |
+
transcript: '',
|
24 |
+
outputText: '',
|
25 |
+
isRecording: false,
|
26 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
27 |
+
},
|
28 |
+
|
29 |
+
onLoad: function () {
|
30 |
+
this.setData({
|
31 |
+
sourceLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: lang.code.split('_')[0]})),
|
32 |
+
targetLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: lang.code.split('_')[0]}))
|
33 |
+
});
|
34 |
+
this.initPluginManager();
|
35 |
+
this.initNativeRecorderManager();
|
36 |
+
},
|
37 |
+
|
38 |
+
// --- Language Selection & UI ---
|
39 |
+
selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }); },
|
40 |
+
selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }); },
|
41 |
+
swapLanguages: function () {
|
42 |
+
this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript });
|
43 |
+
},
|
44 |
+
|
45 |
+
// --- Manager Initializations ---
|
46 |
+
initPluginManager: function () {
|
47 |
+
pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' });
|
48 |
+
pluginManager.onRecognize = (res) => this.setData({ transcript: res.result });
|
49 |
+
pluginManager.onStop = (res) => {
|
50 |
+
this.setData({ isRecording: false });
|
51 |
+
if (!res.result) { return this.setData({ transcript: '识别结果为空' }); }
|
52 |
+
this.setData({ transcript: res.result });
|
53 |
+
const { sourceLang, targetLang, languages } = this.data;
|
54 |
+
if (sourceLang === targetLang) { return this.setData({ outputText: res.result }); }
|
55 |
+
const isPluginTarget = languages[targetLang].usePlugin;
|
56 |
+
if (isPluginTarget) { this.translateViaPlugin(res.result); }
|
57 |
+
else { this.translateViaHfBridge(res.result, sourceLang, targetLang); }
|
58 |
+
};
|
59 |
+
pluginManager.onError = (res) => { this.setData({ isRecording: false }); showDetailedError('插件录音失败', res); };
|
60 |
+
},
|
61 |
+
|
62 |
+
initNativeRecorderManager: function () {
|
63 |
+
nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' });
|
64 |
+
nativeRecorderManager.onStop = (res) => {
|
65 |
+
this.setData({ isRecording: false });
|
66 |
+
if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); }
|
67 |
+
else { this.setData({ transcript: '原生录音文件获取失败' }); }
|
68 |
+
};
|
69 |
+
nativeRecorderManager.onError = (res) => { this.setData({ isRecording: false }); showDetailedError('原生录音失败', res); };
|
70 |
+
},
|
71 |
+
|
72 |
+
// --- Main Record Button Handler ---
|
73 |
+
handleRecordToggle: function() {
|
74 |
+
if (this.data.isRecording) { this.stopRecording(); return; }
|
75 |
+
wx.getSetting({
|
76 |
+
success: (res) => {
|
77 |
+
if (!res.authSetting['scope.record']) {
|
78 |
+
wx.authorize({ scope: 'scope.record', success: this.startRecording, fail: (err) => showDetailedError('授权失败', err) });
|
79 |
+
} else { this.startRecording(); }
|
80 |
+
},
|
81 |
+
fail: (err) => showDetailedError('无法获取权限设置', err)
|
82 |
+
});
|
83 |
+
},
|
84 |
+
|
85 |
+
startRecording: function () {
|
86 |
+
const { sourceLang, languages } = this.data;
|
87 |
+
const langConfig = languages[sourceLang];
|
88 |
+
const shouldUsePlugin = langConfig ? langConfig.usePlugin : false;
|
89 |
+
|
90 |
+
// *** ULTIMATE DEBUG LOG ***
|
91 |
+
console.log(`[DEBUG] Start decision: sourceLang=${sourceLang}, langConfig=${JSON.stringify(langConfig)}, shouldUsePlugin=${shouldUsePlugin}`);
|
92 |
+
|
93 |
+
this.setData({ isRecording: true });
|
94 |
+
|
95 |
+
if (shouldUsePlugin) {
|
96 |
+
pluginManager.start({ lang: langConfig.code, translate: false });
|
97 |
+
} else {
|
98 |
+
nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 });
|
99 |
+
}
|
100 |
+
},
|
101 |
+
|
102 |
+
// --- ROBUST Stop Recording ---
|
103 |
+
stopRecording: function () {
|
104 |
+
this.setData({ isRecording: false });
|
105 |
+
try { pluginManager.stop(); } catch (e) { console.log("Plugin manager wasn't running or failed to stop."); }
|
106 |
+
try { nativeRecorderManager.stop(); } catch (e) { console.log("Native manager wasn't running or failed to stop."); }
|
107 |
+
},
|
108 |
+
|
109 |
+
// --- Translation Logic ---
|
110 |
+
translateViaPlugin: function(text) {
|
111 |
+
const { sourceLang, targetLang, languages } = this.data;
|
112 |
+
this.setData({ outputText: '翻译中 (插件)...' });
|
113 |
+
plugin.translate({
|
114 |
+
lfrom: languages[sourceLang].code, lto: languages[targetLang].code, content: text,
|
115 |
+
success: (res) => { if (res.retcode === 0) { this.setData({ outputText: res.result }); } else { showDetailedError('插件翻译失败', res); } },
|
116 |
+
fail: (err) => showDetailedError('插件翻译接口调用失败', err)
|
117 |
+
});
|
118 |
+
},
|
119 |
+
|
120 |
+
uploadAudioForASR: function (filePath) {
|
121 |
+
this.setData({ transcript: '正在识别 (HF)...' });
|
122 |
+
wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => {
|
123 |
+
wx.request({
|
124 |
+
url: `${this.data.hfSpaceUrl}/api/asr`, method: 'POST', data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, timeout: 60000,
|
125 |
+
success: (asrRes) => {
|
126 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
127 |
+
const transcript = asrRes.data.transcript;
|
128 |
+
this.setData({ transcript });
|
129 |
+
this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
130 |
+
} else { showDetailedError('HF识别失败', asrRes.data); }
|
131 |
+
},
|
132 |
+
fail: (err) => showDetailedError('HF识别请求失败', err)
|
133 |
+
});
|
134 |
+
}});
|
135 |
+
},
|
136 |
+
|
137 |
+
translateViaHfBridge: function(text, source, target) {
|
138 |
+
if (source === target) { return this.setData({ outputText: text }); }
|
139 |
+
this.setData({ outputText: '翻译中 (HF)...' });
|
140 |
+
this.translateViaHF(text, source, target, (result) => { if (result) { this.setData({ outputText: result }); } });
|
141 |
+
},
|
142 |
+
|
143 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
144 |
+
wx.request({
|
145 |
+
url: `${this.data.hfSpaceUrl}/api/translate`, method: 'POST', data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, timeout: 45000,
|
146 |
+
success: (res) => { if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); } else { showDetailedError('HF翻译失败', res.data); callback(null); } },
|
147 |
+
fail: (err) => { showDetailedError('HF翻译请求失败', err); callback(null); }
|
148 |
+
});
|
149 |
+
}
|
150 |
+
});
|
pages/index/index_v27_old.js
ADDED
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const pluginManager = plugin.getRecordRecognitionManager();
|
3 |
+
const nativeRecorderManager = wx.getRecorderManager();
|
4 |
+
|
5 |
+
function showDetailedError(title, content) {
|
6 |
+
wx.showModal({
|
7 |
+
title: title,
|
8 |
+
content: typeof content === 'object' ? JSON.stringify(content) : String(content),
|
9 |
+
showCancel: false
|
10 |
+
});
|
11 |
+
}
|
12 |
+
|
13 |
+
Page({
|
14 |
+
data: {
|
15 |
+
languages: {
|
16 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true },
|
17 |
+
'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true },
|
18 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false },
|
19 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false }
|
20 |
+
},
|
21 |
+
sourceLang: 'zh',
|
22 |
+
targetLang: 'en',
|
23 |
+
transcript: '',
|
24 |
+
outputText: '',
|
25 |
+
isRecording: false,
|
26 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
27 |
+
},
|
28 |
+
|
29 |
+
onLoad: function () {
|
30 |
+
this.setData({
|
31 |
+
sourceLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: lang.code.split('_')[0]})),
|
32 |
+
targetLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: lang.code.split('_')[0]}))
|
33 |
+
});
|
34 |
+
this.initPluginManager();
|
35 |
+
this.initNativeRecorderManager();
|
36 |
+
},
|
37 |
+
|
38 |
+
// --- Language Selection & UI with DEBUG ---
|
39 |
+
selectSourceLanguage: function (e) {
|
40 |
+
const newLang = e.currentTarget.dataset.langCode;
|
41 |
+
console.log(`[DEBUG] Language button tapped. Changing sourceLang to: ${newLang}`);
|
42 |
+
this.setData({ sourceLang: newLang });
|
43 |
+
},
|
44 |
+
selectTargetLanguage: function (e) {
|
45 |
+
const newLang = e.currentTarget.dataset.langCode;
|
46 |
+
console.log(`[DEBUG] Language button tapped. Changing targetLang to: ${newLang}`);
|
47 |
+
this.setData({ targetLang: newLang });
|
48 |
+
},
|
49 |
+
swapLanguages: function () {
|
50 |
+
this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript });
|
51 |
+
},
|
52 |
+
|
53 |
+
// --- Manager Initializations ---
|
54 |
+
initPluginManager: function () {
|
55 |
+
pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' });
|
56 |
+
pluginManager.onRecognize = (res) => this.setData({ transcript: res.result });
|
57 |
+
pluginManager.onStop = (res) => {
|
58 |
+
this.setData({ isRecording: false });
|
59 |
+
if (!res.result) { return this.setData({ transcript: '识别结果为空' }); }
|
60 |
+
this.setData({ transcript: res.result });
|
61 |
+
const { sourceLang, targetLang, languages } = this.data;
|
62 |
+
if (sourceLang === targetLang) { return this.setData({ outputText: res.result }); }
|
63 |
+
const isPluginTarget = languages[targetLang].usePlugin;
|
64 |
+
if (isPluginTarget) { this.translateViaPlugin(res.result); }
|
65 |
+
else { this.translateViaHfBridge(res.result, sourceLang, targetLang); }
|
66 |
+
};
|
67 |
+
pluginManager.onError = (res) => { this.setData({ isRecording: false }); showDetailedError('插件录音失败', res); };
|
68 |
+
},
|
69 |
+
|
70 |
+
initNativeRecorderManager: function () {
|
71 |
+
nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' });
|
72 |
+
nativeRecorderManager.onStop = (res) => {
|
73 |
+
this.setData({ isRecording: false });
|
74 |
+
if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); }
|
75 |
+
else { this.setData({ transcript: '原生录音文件获取失败' }); }
|
76 |
+
};
|
77 |
+
nativeRecorderManager.onError = (res) => { this.setData({ isRecording: false }); showDetailedError('原生录音失败', res); };
|
78 |
+
},
|
79 |
+
|
80 |
+
// --- Main Record Button Handler ---
|
81 |
+
handleRecordToggle: function() {
|
82 |
+
if (this.data.isRecording) { this.stopRecording(); return; }
|
83 |
+
wx.getSetting({
|
84 |
+
success: (res) => {
|
85 |
+
if (!res.authSetting['scope.record']) {
|
86 |
+
wx.authorize({ scope: 'scope.record', success: this.startRecording, fail: (err) => showDetailedError('授权失败', err) });
|
87 |
+
} else { this.startRecording(); }
|
88 |
+
},
|
89 |
+
fail: (err) => showDetailedError('无法获取权限设置', err)
|
90 |
+
});
|
91 |
+
},
|
92 |
+
|
93 |
+
startRecording: function () {
|
94 |
+
const { sourceLang, languages } = this.data;
|
95 |
+
const langConfig = languages[sourceLang];
|
96 |
+
// Fallback to a default config if something goes wrong, to prevent crashes
|
97 |
+
const shouldUsePlugin = langConfig ? langConfig.usePlugin : false;
|
98 |
+
|
99 |
+
// *** ULTIMATE DEBUG LOG ***
|
100 |
+
console.log(`[DEBUG] Start decision: sourceLang=${sourceLang}, langConfig=${JSON.stringify(langConfig)}, shouldUsePlugin=${shouldUsePlugin}`);
|
101 |
+
|
102 |
+
this.setData({ isRecording: true });
|
103 |
+
|
104 |
+
if (shouldUsePlugin) {
|
105 |
+
pluginManager.start({ lang: (langConfig || {code: 'zh_CN'}).code, translate: false });
|
106 |
+
} else {
|
107 |
+
nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 });
|
108 |
+
}
|
109 |
+
},
|
110 |
+
|
111 |
+
// --- ROBUST Stop Recording ---
|
112 |
+
stopRecording: function () {
|
113 |
+
this.setData({ isRecording: false });
|
114 |
+
try { pluginManager.stop(); } catch (e) { /* Ignore error */ }
|
115 |
+
try { nativeRecorderManager.stop(); } catch (e) { /* Ignore error */ }
|
116 |
+
},
|
117 |
+
|
118 |
+
// --- Translation Logic ---
|
119 |
+
translateViaPlugin: function(text) {
|
120 |
+
const { sourceLang, targetLang, languages } = this.data;
|
121 |
+
this.setData({ outputText: '翻译中 (插件)...' });
|
122 |
+
plugin.translate({
|
123 |
+
lfrom: languages[sourceLang].code, lto: languages[targetLang].code, content: text,
|
124 |
+
success: (res) => { if (res.retcode === 0) { this.setData({ outputText: res.result }); } else { showDetailedError('插件翻译失败', res); } },
|
125 |
+
fail: (err) => showDetailedError('插件翻译接口调用失败', err)
|
126 |
+
});
|
127 |
+
},
|
128 |
+
|
129 |
+
uploadAudioForASR: function (filePath) {
|
130 |
+
this.setData({ transcript: '正在识别 (HF)...' });
|
131 |
+
wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => {
|
132 |
+
wx.request({
|
133 |
+
url: `${this.data.hfSpaceUrl}/api/asr`, method: 'POST', data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, timeout: 60000,
|
134 |
+
success: (asrRes) => {
|
135 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
136 |
+
const transcript = asrRes.data.transcript;
|
137 |
+
this.setData({ transcript });
|
138 |
+
this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
139 |
+
} else { showDetailedError('HF识别失败', asrRes.data); }
|
140 |
+
},
|
141 |
+
fail: (err) => showDetailedError('HF识别请求失败', err)
|
142 |
+
});
|
143 |
+
}});
|
144 |
+
},
|
145 |
+
|
146 |
+
translateViaHfBridge: function(text, source, target) {
|
147 |
+
if (source === target) { return this.setData({ outputText: text }); }
|
148 |
+
this.setData({ outputText: '翻译中 (HF)...' });
|
149 |
+
this.translateViaHF(text, source, target, (result) => { if (result) { this.setData({ outputText: result }); } });
|
150 |
+
},
|
151 |
+
|
152 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
153 |
+
wx.request({
|
154 |
+
url: `${this.data.hfSpaceUrl}/api/translate`, method: 'POST', data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, timeout: 45000,
|
155 |
+
success: (res) => { if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); } else { showDetailedError('HF翻译失败', res.data); callback(null); } },
|
156 |
+
fail: (err) => { showDetailedError('HF翻译请求失败', err); callback(null); }
|
157 |
+
});
|
158 |
+
}
|
159 |
+
});
|
pages/index/index_v28_old.js
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// FINAL VERSION: v28 - Unified Native Recording and HF Backend
|
2 |
+
const nativeRecorderManager = wx.getRecorderManager();
|
3 |
+
|
4 |
+
// Helper function to show detailed errors
|
5 |
+
function showDetailedError(title, content) {
|
6 |
+
wx.showModal({
|
7 |
+
title: title,
|
8 |
+
content: typeof content === 'object' ? JSON.stringify(content) : String(content),
|
9 |
+
showCancel: false
|
10 |
+
});
|
11 |
+
}
|
12 |
+
|
13 |
+
Page({
|
14 |
+
data: {
|
15 |
+
languages: {
|
16 |
+
'zh': { name: '中文', flag: 'cn' },
|
17 |
+
'en': { name: 'English', flag: 'us' },
|
18 |
+
'ja': { name: '日本語', flag: 'jp' },
|
19 |
+
'ko': { name: '한국어', flag: 'kr' }
|
20 |
+
},
|
21 |
+
sourceLang: 'zh',
|
22 |
+
targetLang: 'en',
|
23 |
+
transcript: '',
|
24 |
+
outputText: '',
|
25 |
+
isRecording: false,
|
26 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
27 |
+
},
|
28 |
+
|
29 |
+
onLoad: function () {
|
30 |
+
// Directly create language arrays for the UI from the static config
|
31 |
+
this.setData({
|
32 |
+
sourceLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key })),
|
33 |
+
targetLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key }))
|
34 |
+
});
|
35 |
+
this.initNativeRecorderManager();
|
36 |
+
},
|
37 |
+
|
38 |
+
// --- Language Selection & UI ---
|
39 |
+
selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }); },
|
40 |
+
selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }); },
|
41 |
+
swapLanguages: function () {
|
42 |
+
this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript });
|
43 |
+
},
|
44 |
+
|
45 |
+
// --- Unified Native Recorder Initialization ---
|
46 |
+
initNativeRecorderManager: function () {
|
47 |
+
nativeRecorderManager.onStart = () => {
|
48 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
49 |
+
};
|
50 |
+
nativeRecorderManager.onStop = (res) => {
|
51 |
+
this.setData({ isRecording: false });
|
52 |
+
if (res.tempFilePath) {
|
53 |
+
// ALL recordings go to the HF backend for ASR
|
54 |
+
this.uploadAudioForASR(res.tempFilePath);
|
55 |
+
} else {
|
56 |
+
showDetailedError('录音失败', '未能获取到有效的录音文件路径。');
|
57 |
+
}
|
58 |
+
};
|
59 |
+
nativeRecorderManager.onError = (res) => {
|
60 |
+
this.setData({ isRecording: false });
|
61 |
+
showDetailedError('录音发生错误', res);
|
62 |
+
};
|
63 |
+
},
|
64 |
+
|
65 |
+
// --- Main Record Button Handler ---
|
66 |
+
handleRecordToggle: function() {
|
67 |
+
if (this.data.isRecording) {
|
68 |
+
this.stopRecording();
|
69 |
+
return;
|
70 |
+
}
|
71 |
+
// Check permissions before starting
|
72 |
+
wx.getSetting({
|
73 |
+
success: (res) => {
|
74 |
+
if (!res.authSetting['scope.record']) {
|
75 |
+
wx.authorize({ scope: 'scope.record', success: this.startRecording, fail: (err) => showDetailedError('授权失败', err) });
|
76 |
+
} else {
|
77 |
+
this.startRecording();
|
78 |
+
}
|
79 |
+
},
|
80 |
+
fail: (err) => showDetailedError('无法获取权限设置', err)
|
81 |
+
});
|
82 |
+
},
|
83 |
+
|
84 |
+
// --- Unified Start/Stop Recording ---
|
85 |
+
startRecording: function () {
|
86 |
+
this.setData({ isRecording: true });
|
87 |
+
nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 });
|
88 |
+
},
|
89 |
+
|
90 |
+
stopRecording: function () {
|
91 |
+
this.setData({ isRecording: false });
|
92 |
+
nativeRecorderManager.stop();
|
93 |
+
},
|
94 |
+
|
95 |
+
// --- Unified Backend ASR & Translation Flow ---
|
96 |
+
uploadAudioForASR: function (filePath) {
|
97 |
+
this.setData({ transcript: '正在识别 (1/2)...' });
|
98 |
+
wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => {
|
99 |
+
wx.request({
|
100 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
101 |
+
method: 'POST',
|
102 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
103 |
+
timeout: 60000,
|
104 |
+
success: (asrRes) => {
|
105 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
106 |
+
const transcript = asrRes.data.transcript;
|
107 |
+
this.setData({ transcript });
|
108 |
+
// After ASR, ALL translations go to the HF backend
|
109 |
+
this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
110 |
+
} else {
|
111 |
+
showDetailedError('语音识别失败', asrRes.data);
|
112 |
+
}
|
113 |
+
},
|
114 |
+
fail: (err) => showDetailedError('识别请求失败', err)
|
115 |
+
});
|
116 |
+
}});
|
117 |
+
},
|
118 |
+
|
119 |
+
translateViaHfBridge: function(text, source, target) {
|
120 |
+
if (source === target) {
|
121 |
+
return this.setData({ outputText: text });
|
122 |
+
}
|
123 |
+
this.setData({ outputText: '正在翻译 (2/2)...' });
|
124 |
+
wx.request({
|
125 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
126 |
+
method: 'POST',
|
127 |
+
data: { "text": text, "source_lang": source, "target_lang": target },
|
128 |
+
timeout: 45000,
|
129 |
+
success: (res) => {
|
130 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
131 |
+
this.setData({ outputText: res.data.translated_text });
|
132 |
+
} else {
|
133 |
+
showDetailedError('翻译失败', res.data);
|
134 |
+
}
|
135 |
+
},
|
136 |
+
fail: (err) => showDetailedError('翻译请求失败', err)
|
137 |
+
});
|
138 |
+
}
|
139 |
+
});
|
pages/index/index_v2_old.js
ADDED
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
const innerAudioContext = wx.createInnerAudioContext();
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
9 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
10 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
11 |
+
},
|
12 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
13 |
+
sourceLang: 'zh',
|
14 |
+
targetLang: 'en',
|
15 |
+
transcript: '',
|
16 |
+
outputText: '',
|
17 |
+
isRecording: false,
|
18 |
+
sourceLanguages: [],
|
19 |
+
targetLanguages: [],
|
20 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.recorderManager = wx.getRecorderManager();
|
26 |
+
this.initRecorderManager();
|
27 |
+
plugin.getRecordRecognitionManager = wx.getRecordRecognitionManager;
|
28 |
+
},
|
29 |
+
|
30 |
+
initializeLanguages: function () {
|
31 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
32 |
+
const sourceLanguages = langCodes.map(code => ({
|
33 |
+
langCode: code,
|
34 |
+
name: languages[code].name,
|
35 |
+
flag: languages[code].flag,
|
36 |
+
selected: code === sourceLang
|
37 |
+
}));
|
38 |
+
const targetLanguages = langCodes.map(code => ({
|
39 |
+
langCode: code,
|
40 |
+
name: languages[code].name,
|
41 |
+
flag: languages[code].flag,
|
42 |
+
selected: code === targetLang
|
43 |
+
}));
|
44 |
+
this.setData({ sourceLanguages, targetLanguages });
|
45 |
+
},
|
46 |
+
|
47 |
+
selectSourceLanguage: function (e) {
|
48 |
+
const newSourceLang = e.currentTarget.dataset.langCode;
|
49 |
+
this.setData({ sourceLang: newSourceLang }, () => {
|
50 |
+
this.initializeLanguages();
|
51 |
+
if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') {
|
52 |
+
this.translate(this.data.transcript);
|
53 |
+
}
|
54 |
+
});
|
55 |
+
},
|
56 |
+
|
57 |
+
selectTargetLanguage: function (e) {
|
58 |
+
const newTargetLang = e.currentTarget.dataset.langCode;
|
59 |
+
this.setData({ targetLang: newTargetLang }, () => {
|
60 |
+
this.initializeLanguages();
|
61 |
+
if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') {
|
62 |
+
this.translate(this.data.transcript);
|
63 |
+
}
|
64 |
+
});
|
65 |
+
},
|
66 |
+
|
67 |
+
swapLanguages: function () {
|
68 |
+
let { sourceLang, targetLang, transcript, outputText } = this.data;
|
69 |
+
const tempLang = sourceLang;
|
70 |
+
sourceLang = targetLang;
|
71 |
+
targetLang = tempLang;
|
72 |
+
const tempText = transcript;
|
73 |
+
transcript = outputText;
|
74 |
+
outputText = tempText;
|
75 |
+
this.setData({ sourceLang, targetLang, transcript, outputText }, () => {
|
76 |
+
this.initializeLanguages();
|
77 |
+
if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') {
|
78 |
+
this.translate(this.data.transcript);
|
79 |
+
}
|
80 |
+
});
|
81 |
+
},
|
82 |
+
|
83 |
+
initRecorderManager: function () {
|
84 |
+
this.recorderManager.onStart(() => {
|
85 |
+
this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' });
|
86 |
+
});
|
87 |
+
|
88 |
+
this.recorderManager.onStop((res) => {
|
89 |
+
this.setData({ isRecording: false });
|
90 |
+
if (res.tempFilePath) {
|
91 |
+
this.uploadAudioForASR(res.tempFilePath);
|
92 |
+
} else {
|
93 |
+
this.setData({ transcript: '录音文件创建失败' });
|
94 |
+
}
|
95 |
+
});
|
96 |
+
|
97 |
+
this.recorderManager.onError(() => {
|
98 |
+
this.setData({ isRecording: false, transcript: '语音识别出错' });
|
99 |
+
});
|
100 |
+
},
|
101 |
+
|
102 |
+
startRecording: function () {
|
103 |
+
this.recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' });
|
104 |
+
},
|
105 |
+
|
106 |
+
stopRecording: function () {
|
107 |
+
this.recorderManager.stop();
|
108 |
+
},
|
109 |
+
|
110 |
+
uploadAudioForASR: function (filePath) {
|
111 |
+
this.setData({ transcript: '正在识别...' });
|
112 |
+
const fileSystemManager = wx.getFileSystemManager();
|
113 |
+
fileSystemManager.readFile({
|
114 |
+
filePath: filePath,
|
115 |
+
encoding: 'base64',
|
116 |
+
success: (res) => {
|
117 |
+
const base64Data = res.data;
|
118 |
+
const dataUri = 'data:audio/mp3;base64,' + base64Data;
|
119 |
+
|
120 |
+
wx.request({
|
121 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
122 |
+
method: 'POST',
|
123 |
+
header: { 'Content-Type': 'application/json' },
|
124 |
+
data: { "audio_data_uri": dataUri },
|
125 |
+
timeout: 60000,
|
126 |
+
success: (res) => {
|
127 |
+
if (res.statusCode === 200 && res.data && res.data.transcript) {
|
128 |
+
const transcript = res.data.transcript;
|
129 |
+
this.setData({ transcript: transcript });
|
130 |
+
this.translate(transcript);
|
131 |
+
} else {
|
132 |
+
this.setData({ transcript: '语音识别失败' });
|
133 |
+
}
|
134 |
+
},
|
135 |
+
fail: () => {
|
136 |
+
this.setData({ transcript: '语音识别请求失败' });
|
137 |
+
}
|
138 |
+
});
|
139 |
+
},
|
140 |
+
fail: () => {
|
141 |
+
this.setData({ transcript: '读取音频文件失败' });
|
142 |
+
}
|
143 |
+
});
|
144 |
+
},
|
145 |
+
|
146 |
+
translate: function (text) {
|
147 |
+
if (!text) return;
|
148 |
+
const { sourceLang, targetLang, languages } = this.data;
|
149 |
+
|
150 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
151 |
+
|
152 |
+
if (isChineseEnglish) {
|
153 |
+
// --- Scenario 1: Direct CN-EN translation using WeChat Plugin ---
|
154 |
+
this.setData({ outputText: '正在翻译 (微信)...' });
|
155 |
+
plugin.translate({
|
156 |
+
lfrom: languages[sourceLang].code,
|
157 |
+
lto: languages[targetLang].code,
|
158 |
+
content: text,
|
159 |
+
success: (res) => {
|
160 |
+
if (res.retcode == 0) {
|
161 |
+
this.setData({ outputText: res.result });
|
162 |
+
} else {
|
163 |
+
this.setData({ outputText: '微信翻译失败' });
|
164 |
+
}
|
165 |
+
},
|
166 |
+
fail: () => {
|
167 |
+
this.setData({ outputText: '微信翻译接口调用失败' });
|
168 |
+
}
|
169 |
+
});
|
170 |
+
} else {
|
171 |
+
// --- Scenario 2: Hybrid translation for other languages ---
|
172 |
+
this.hybridTranslate(text, sourceLang, targetLang);
|
173 |
+
}
|
174 |
+
},
|
175 |
+
|
176 |
+
hybridTranslate: function(text, sourceLang, targetLang) {
|
177 |
+
// If source is not English, first translate to English using WeChat Plugin
|
178 |
+
if (sourceLang !== 'en') {
|
179 |
+
this.setData({ outputText: '翻译中 (1/2)...' });
|
180 |
+
plugin.translate({
|
181 |
+
lfrom: this.data.languages[sourceLang].code,
|
182 |
+
lto: 'en_US',
|
183 |
+
content: text,
|
184 |
+
success: (res) => {
|
185 |
+
if (res.retcode == 0) {
|
186 |
+
const englishText = res.result;
|
187 |
+
// If the final target is English, we are done.
|
188 |
+
if (targetLang === 'en') {
|
189 |
+
this.setData({ outputText: englishText });
|
190 |
+
} else {
|
191 |
+
// Otherwise, send the high-quality English text to HF for the second leg
|
192 |
+
this.translateViaHF(englishText, 'en', targetLang);
|
193 |
+
}
|
194 |
+
} else {
|
195 |
+
this.setData({ outputText: '混合翻译失败 (1/2)' });
|
196 |
+
}
|
197 |
+
},
|
198 |
+
fail: () => {
|
199 |
+
this.setData({ outputText: '混合翻译接口调用失败 (1/2)' });
|
200 |
+
}
|
201 |
+
});
|
202 |
+
} else {
|
203 |
+
// If source is already English, directly call HF for translation
|
204 |
+
this.translateViaHF(text, sourceLang, targetLang);
|
205 |
+
}
|
206 |
+
},
|
207 |
+
|
208 |
+
translateViaHF: function(text, sourceLang, targetLang) {
|
209 |
+
this.setData({ outputText: '翻译中 (2/2)..' });
|
210 |
+
wx.request({
|
211 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
212 |
+
method: 'POST',
|
213 |
+
header: { 'Content-Type': 'application/json' },
|
214 |
+
data: {
|
215 |
+
"text": text,
|
216 |
+
"source_lang": sourceLang,
|
217 |
+
"target_lang": targetLang
|
218 |
+
},
|
219 |
+
timeout: 30000,
|
220 |
+
success: (res) => {
|
221 |
+
if (res.statusCode === 200 && res.data && res.data.translated_text) {
|
222 |
+
this.setData({ outputText: res.data.translated_text });
|
223 |
+
} else {
|
224 |
+
this.setData({ outputText: '混合翻译失败 (2/2)' });
|
225 |
+
}
|
226 |
+
},
|
227 |
+
fail: () => {
|
228 |
+
this.setData({ outputText: '混合翻译出错 (2/2)' });
|
229 |
+
}
|
230 |
+
});
|
231 |
+
}
|
232 |
+
});
|
pages/index/index_v3_old.js
ADDED
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
const innerAudioContext = wx.createInnerAudioContext();
|
3 |
+
|
4 |
+
// Get the voice recognition manager from the plugin
|
5 |
+
const manager = plugin.getRecordRecognitionManager();
|
6 |
+
|
7 |
+
Page({
|
8 |
+
data: {
|
9 |
+
languages: {
|
10 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
11 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
12 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
13 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
14 |
+
},
|
15 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
16 |
+
sourceLang: 'zh',
|
17 |
+
targetLang: 'en',
|
18 |
+
transcript: '',
|
19 |
+
outputText: '',
|
20 |
+
isRecording: false,
|
21 |
+
sourceLanguages: [],
|
22 |
+
targetLanguages: [],
|
23 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
24 |
+
currentRecordMode: '' // 'plugin' or 'custom'
|
25 |
+
},
|
26 |
+
|
27 |
+
onLoad: function () {
|
28 |
+
this.initializeLanguages();
|
29 |
+
// Custom recorder (for HF)
|
30 |
+
this.customRecorderManager = wx.getRecorderManager();
|
31 |
+
this.initCustomRecorderManager();
|
32 |
+
// Plugin recorder (for CN/EN)
|
33 |
+
this.initPluginRecorderManager();
|
34 |
+
},
|
35 |
+
|
36 |
+
// --- Language Selection Logic ---
|
37 |
+
initializeLanguages: function () {
|
38 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
39 |
+
this.setData({
|
40 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
41 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
42 |
+
});
|
43 |
+
},
|
44 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
45 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
46 |
+
swapLanguages: function() {
|
47 |
+
this.setData({
|
48 |
+
sourceLang: this.data.targetLang,
|
49 |
+
targetLang: this.data.sourceLang,
|
50 |
+
transcript: this.data.outputText,
|
51 |
+
outputText: this.data.transcript
|
52 |
+
}, this.initializeLanguages);
|
53 |
+
},
|
54 |
+
|
55 |
+
// --- Recording Logic ---
|
56 |
+
startRecording: function () {
|
57 |
+
const { sourceLang, targetLang } = this.data;
|
58 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
59 |
+
|
60 |
+
this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' });
|
61 |
+
|
62 |
+
if (isChineseEnglish) {
|
63 |
+
// Use the ultra-fast WeChat Plugin for both ASR and Translation
|
64 |
+
this.setData({ currentRecordMode: 'plugin' });
|
65 |
+
manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code });
|
66 |
+
} else {
|
67 |
+
// Use our custom HF backend for ASR for other languages
|
68 |
+
this.setData({ currentRecordMode: 'custom' });
|
69 |
+
this.customRecorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' });
|
70 |
+
}
|
71 |
+
},
|
72 |
+
|
73 |
+
stopRecording: function () {
|
74 |
+
if (this.data.currentRecordMode === 'plugin') {
|
75 |
+
manager.stop();
|
76 |
+
} else if (this.data.currentRecordMode === 'custom') {
|
77 |
+
this.customRecorderManager.stop();
|
78 |
+
}
|
79 |
+
this.setData({ isRecording: false });
|
80 |
+
},
|
81 |
+
|
82 |
+
// --- Result Handlers ---
|
83 |
+
initPluginRecorderManager: function() {
|
84 |
+
manager.onStart = (res) => {
|
85 |
+
this.setData({ transcript: '正在识别和翻译...' });
|
86 |
+
};
|
87 |
+
manager.onRecognize = (res) => {
|
88 |
+
this.setData({ transcript: res.result });
|
89 |
+
};
|
90 |
+
manager.onStop = (res) => {
|
91 |
+
this.setData({ transcript: res.result, outputText: res.translateResult });
|
92 |
+
};
|
93 |
+
manager.onError = (res) => {
|
94 |
+
this.setData({ transcript: '微信插件识别失败', outputText: res.msg });
|
95 |
+
};
|
96 |
+
},
|
97 |
+
|
98 |
+
initCustomRecorderManager: function() {
|
99 |
+
this.customRecorderManager.onStop = (res) => {
|
100 |
+
if (res.tempFilePath) {
|
101 |
+
this.uploadAudioForASR(res.tempFilePath);
|
102 |
+
} else {
|
103 |
+
this.setData({ transcript: '录音文件创建失败' });
|
104 |
+
}
|
105 |
+
};
|
106 |
+
this.customRecorderManager.onError = () => {
|
107 |
+
this.setData({ transcript: '语音识别出错' });
|
108 |
+
};
|
109 |
+
},
|
110 |
+
|
111 |
+
// --- Custom Translation Flow (for non-CN/EN pairs) ---
|
112 |
+
uploadAudioForASR: function (filePath) {
|
113 |
+
this.setData({ transcript: '正在识别 (HF)...' });
|
114 |
+
const fileSystemManager = wx.getFileSystemManager();
|
115 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
116 |
+
wx.request({
|
117 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
118 |
+
method: 'POST',
|
119 |
+
header: { 'Content-Type': 'application/json' },
|
120 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
121 |
+
timeout: 60000,
|
122 |
+
success: (asrRes) => {
|
123 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
124 |
+
const transcript = asrRes.data.transcript;
|
125 |
+
this.setData({ transcript });
|
126 |
+
this.hybridTranslate(transcript, this.data.sourceLang, this.data.targetLang);
|
127 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
128 |
+
},
|
129 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
130 |
+
});
|
131 |
+
}});
|
132 |
+
},
|
133 |
+
|
134 |
+
hybridTranslate: function(text, sourceLang, targetLang) {
|
135 |
+
// If source is ZH or EN, use the fast plugin for the first leg of the bridge
|
136 |
+
if (sourceLang === 'zh' || sourceLang === 'en') {
|
137 |
+
this.setData({ outputText: '翻译中 (1/2)...' });
|
138 |
+
plugin.translate({ lfrom: this.data.languages[sourceLang].code, lto: 'en_US', content: text, success: (res) => {
|
139 |
+
if (res.retcode === 0) {
|
140 |
+
this.translateViaHF(res.result, 'en', targetLang);
|
141 |
+
} else { this.setData({ outputText: '混合翻译失败 (1/2)' }); }
|
142 |
+
}});
|
143 |
+
} else {
|
144 |
+
// If source is JA or KO, we MUST use our HF backend for the first leg
|
145 |
+
this.setData({ outputText: '翻译中 (1/2 - HF)..' });
|
146 |
+
this.translateViaHF(text, sourceLang, 'en', (englishText) => {
|
147 |
+
// This is a callback to run after the first leg is complete
|
148 |
+
this.translateViaHF(englishText, 'en', targetLang);
|
149 |
+
});
|
150 |
+
}
|
151 |
+
},
|
152 |
+
|
153 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
154 |
+
this.setData({ outputText: this.data.outputText + ' (2/2)..' });
|
155 |
+
wx.request({
|
156 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
157 |
+
method: 'POST',
|
158 |
+
header: { 'Content-Type': 'application/json' },
|
159 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
160 |
+
timeout: 30000,
|
161 |
+
success: (res) => {
|
162 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
163 |
+
if (callback) {
|
164 |
+
callback(res.data.translated_text);
|
165 |
+
} else {
|
166 |
+
this.setData({ outputText: res.data.translated_text });
|
167 |
+
}
|
168 |
+
} else { this.setData({ outputText: 'HF翻译失败' }); }
|
169 |
+
},
|
170 |
+
fail: () => { this.setData({ outputText: 'HF翻译请求失败' }); }
|
171 |
+
});
|
172 |
+
}
|
173 |
+
});
|
pages/index/index_v4_old.js
ADDED
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
const innerAudioContext = wx.createInnerAudioContext();
|
3 |
+
const pluginManager = plugin.getRecordRecognitionManager();
|
4 |
+
|
5 |
+
Page({
|
6 |
+
data: {
|
7 |
+
languages: {
|
8 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
9 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
10 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
11 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
12 |
+
},
|
13 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
14 |
+
sourceLang: 'zh',
|
15 |
+
targetLang: 'en',
|
16 |
+
transcript: '',
|
17 |
+
outputText: '',
|
18 |
+
isRecording: false,
|
19 |
+
sourceLanguages: [],
|
20 |
+
targetLanguages: [],
|
21 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
22 |
+
currentRecordMode: '' // 'plugin' or 'custom'
|
23 |
+
},
|
24 |
+
|
25 |
+
onLoad: function () {
|
26 |
+
this.initializeLanguages();
|
27 |
+
this.customRecorderManager = wx.getRecorderManager();
|
28 |
+
this.initCustomRecorderManager();
|
29 |
+
this.initPluginRecorderManager();
|
30 |
+
},
|
31 |
+
|
32 |
+
// --- Language Selection Logic ---
|
33 |
+
initializeLanguages: function () {
|
34 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
35 |
+
this.setData({
|
36 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
37 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
38 |
+
});
|
39 |
+
},
|
40 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
41 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
42 |
+
swapLanguages: function() {
|
43 |
+
this.setData({
|
44 |
+
sourceLang: this.data.targetLang,
|
45 |
+
targetLang: this.data.sourceLang,
|
46 |
+
transcript: this.data.outputText,
|
47 |
+
outputText: this.data.transcript
|
48 |
+
}, this.initializeLanguages);
|
49 |
+
},
|
50 |
+
|
51 |
+
// --- Recording Logic ---
|
52 |
+
startRecording: function () {
|
53 |
+
const { sourceLang, targetLang } = this.data;
|
54 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
55 |
+
|
56 |
+
this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' });
|
57 |
+
|
58 |
+
if (isChineseEnglish) {
|
59 |
+
this.setData({ currentRecordMode: 'plugin' });
|
60 |
+
pluginManager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code });
|
61 |
+
} else {
|
62 |
+
this.setData({ currentRecordMode: 'custom' });
|
63 |
+
this.customRecorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' });
|
64 |
+
}
|
65 |
+
},
|
66 |
+
|
67 |
+
stopRecording: function () {
|
68 |
+
if (this.data.currentRecordMode === 'plugin') {
|
69 |
+
pluginManager.stop();
|
70 |
+
} else if (this.data.currentRecordMode === 'custom') {
|
71 |
+
this.customRecorderManager.stop();
|
72 |
+
}
|
73 |
+
this.setData({ isRecording: false });
|
74 |
+
},
|
75 |
+
|
76 |
+
// --- Result Handlers ---
|
77 |
+
initPluginRecorderManager: function() {
|
78 |
+
pluginManager.onRecognize = (res) => {
|
79 |
+
this.setData({ transcript: res.result });
|
80 |
+
};
|
81 |
+
// CORRECTED: Use onTranslate to get translation results
|
82 |
+
pluginManager.onTranslate = (res) => {
|
83 |
+
this.setData({ outputText: res.result });
|
84 |
+
};
|
85 |
+
pluginManager.onStop = (res) => {
|
86 |
+
this.setData({ transcript: res.result, outputText: res.translateResult });
|
87 |
+
};
|
88 |
+
pluginManager.onError = (res) => {
|
89 |
+
this.setData({ transcript: '微信插件识别失败', outputText: res.msg });
|
90 |
+
};
|
91 |
+
},
|
92 |
+
|
93 |
+
initCustomRecorderManager: function() {
|
94 |
+
this.customRecorderManager.onStop = (res) => {
|
95 |
+
if (res.tempFilePath) {
|
96 |
+
this.uploadAudioForASR(res.tempFilePath);
|
97 |
+
} else { this.setData({ transcript: '录音文件创建失败' }); }
|
98 |
+
};
|
99 |
+
this.customRecorderManager.onError = () => {
|
100 |
+
this.setData({ transcript: '语音识别出错' });
|
101 |
+
};
|
102 |
+
},
|
103 |
+
|
104 |
+
// --- Custom Translation Flow (for non-CN/EN pairs) ---
|
105 |
+
uploadAudioForASR: function (filePath) {
|
106 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
107 |
+
const fileSystemManager = wx.getFileSystemManager();
|
108 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
109 |
+
wx.request({
|
110 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
111 |
+
method: 'POST',
|
112 |
+
header: { 'Content-Type': 'application/json' },
|
113 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
114 |
+
timeout: 60000,
|
115 |
+
success: (asrRes) => {
|
116 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
117 |
+
const transcript = asrRes.data.transcript;
|
118 |
+
this.setData({ transcript });
|
119 |
+
// Start the full-backend translation bridge
|
120 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
121 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
122 |
+
},
|
123 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
124 |
+
});
|
125 |
+
}});
|
126 |
+
},
|
127 |
+
|
128 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
129 |
+
// Step 1: Translate source (e.g., JA) to English via HF
|
130 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
131 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
132 |
+
if (englishResult) {
|
133 |
+
// Step 2: Translate English result to final target (e.g., ZH) via HF
|
134 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
135 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
136 |
+
if (finalResult) {
|
137 |
+
this.setData({ outputText: finalResult });
|
138 |
+
}
|
139 |
+
});
|
140 |
+
}
|
141 |
+
});
|
142 |
+
},
|
143 |
+
|
144 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
145 |
+
wx.request({
|
146 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
147 |
+
method: 'POST',
|
148 |
+
header: { 'Content-Type': 'application/json' },
|
149 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
150 |
+
timeout: 30000,
|
151 |
+
success: (res) => {
|
152 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
153 |
+
if (callback) {
|
154 |
+
callback(res.data.translated_text);
|
155 |
+
} else {
|
156 |
+
this.setData({ outputText: res.data.translated_text });
|
157 |
+
}
|
158 |
+
} else {
|
159 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
160 |
+
if (callback) callback(null); // Signal failure
|
161 |
+
}
|
162 |
+
},
|
163 |
+
fail: () => {
|
164 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
165 |
+
if (callback) callback(null); // Signal failure
|
166 |
+
}
|
167 |
+
});
|
168 |
+
}
|
169 |
+
});
|
pages/index/index_v5_old.js
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
const innerAudioContext = wx.createInnerAudioContext();
|
3 |
+
const manager = plugin.getRecordRecognitionManager();
|
4 |
+
|
5 |
+
Page({
|
6 |
+
data: {
|
7 |
+
languages: {
|
8 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
9 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
10 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
11 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
12 |
+
},
|
13 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
14 |
+
sourceLang: 'zh',
|
15 |
+
targetLang: 'en',
|
16 |
+
transcript: '',
|
17 |
+
outputText: '',
|
18 |
+
isRecording: false,
|
19 |
+
sourceLanguages: [],
|
20 |
+
targetLanguages: [],
|
21 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
22 |
+
currentMode: '' // 'plugin-translate' or 'hf-bridge'
|
23 |
+
},
|
24 |
+
|
25 |
+
onLoad: function () {
|
26 |
+
this.initializeLanguages();
|
27 |
+
this.initRecorderManager();
|
28 |
+
},
|
29 |
+
|
30 |
+
// --- Language Selection Logic ---
|
31 |
+
initializeLanguages: function () {
|
32 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
33 |
+
this.setData({
|
34 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
35 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
36 |
+
});
|
37 |
+
},
|
38 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
39 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
40 |
+
swapLanguages: function() {
|
41 |
+
this.setData({
|
42 |
+
sourceLang: this.data.targetLang,
|
43 |
+
targetLang: this.data.sourceLang,
|
44 |
+
transcript: this.data.outputText,
|
45 |
+
outputText: this.data.transcript
|
46 |
+
}, this.initializeLanguages);
|
47 |
+
},
|
48 |
+
|
49 |
+
// --- Unified Recording Logic ---
|
50 |
+
initRecorderManager: function() {
|
51 |
+
manager.onStart = (res) => {
|
52 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
53 |
+
};
|
54 |
+
|
55 |
+
manager.onRecognize = (res) => {
|
56 |
+
this.setData({ transcript: res.result });
|
57 |
+
};
|
58 |
+
|
59 |
+
manager.onTranslate = (res) => {
|
60 |
+
this.setData({ outputText: res.result });
|
61 |
+
};
|
62 |
+
|
63 |
+
manager.onStop = (res) => {
|
64 |
+
this.setData({ isRecording: false });
|
65 |
+
if (this.data.currentMode === 'plugin-translate') {
|
66 |
+
this.setData({ transcript: res.result, outputText: res.translateResult });
|
67 |
+
} else if (this.data.currentMode === 'hf-bridge') {
|
68 |
+
// For HF mode, we need the audio file to upload.
|
69 |
+
if (res.tempFilePath) {
|
70 |
+
this.uploadAudioForASR(res.tempFilePath);
|
71 |
+
} else {
|
72 |
+
this.setData({ transcript: '录音文件获取失败' });
|
73 |
+
}
|
74 |
+
}
|
75 |
+
};
|
76 |
+
|
77 |
+
manager.onError = (res) => {
|
78 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
79 |
+
};
|
80 |
+
},
|
81 |
+
|
82 |
+
startRecording: function () {
|
83 |
+
const { sourceLang, targetLang } = this.data;
|
84 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
85 |
+
|
86 |
+
this.setData({ isRecording: true });
|
87 |
+
|
88 |
+
if (isChineseEnglish) {
|
89 |
+
// Mode 1: Fast, all-in-one ASR and Translation by the plugin
|
90 |
+
this.setData({ currentMode: 'plugin-translate' });
|
91 |
+
manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code });
|
92 |
+
} else {
|
93 |
+
// Mode 2: HF Bridge. Use plugin for ASR only, then our backend for translation.
|
94 |
+
this.setData({ currentMode: 'hf-bridge' });
|
95 |
+
// We ask the plugin to record, but NOT to translate.
|
96 |
+
manager.start({ lang: this.data.languages[sourceLang].code, translate: false });
|
97 |
+
}
|
98 |
+
},
|
99 |
+
|
100 |
+
stopRecording: function () {
|
101 |
+
manager.stop();
|
102 |
+
},
|
103 |
+
|
104 |
+
// --- HF Bridge Translation Flow ---
|
105 |
+
uploadAudioForASR: function (filePath) {
|
106 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
107 |
+
const fileSystemManager = wx.getFileSystemManager();
|
108 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
109 |
+
wx.request({
|
110 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
111 |
+
method: 'POST',
|
112 |
+
header: { 'Content-Type': 'application/json' },
|
113 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
114 |
+
timeout: 60000,
|
115 |
+
success: (asrRes) => {
|
116 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
117 |
+
const transcript = asrRes.data.transcript;
|
118 |
+
this.setData({ transcript });
|
119 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
120 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
121 |
+
},
|
122 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
123 |
+
});
|
124 |
+
}});
|
125 |
+
},
|
126 |
+
|
127 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
128 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
129 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
130 |
+
if (englishResult) {
|
131 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
132 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
133 |
+
if (finalResult) {
|
134 |
+
this.setData({ outputText: finalResult });
|
135 |
+
}
|
136 |
+
});
|
137 |
+
}
|
138 |
+
});
|
139 |
+
},
|
140 |
+
|
141 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
142 |
+
wx.request({
|
143 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
144 |
+
method: 'POST',
|
145 |
+
header: { 'Content-Type': 'application/json' },
|
146 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
147 |
+
timeout: 30000,
|
148 |
+
success: (res) => {
|
149 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
150 |
+
callback(res.data.translated_text);
|
151 |
+
} else {
|
152 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
153 |
+
callback(null);
|
154 |
+
}
|
155 |
+
},
|
156 |
+
fail: () => {
|
157 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
158 |
+
callback(null);
|
159 |
+
}
|
160 |
+
});
|
161 |
+
}
|
162 |
+
});
|
pages/index/index_v6_old.js
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
const manager = plugin.getRecordRecognitionManager();
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
9 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
10 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
11 |
+
},
|
12 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
13 |
+
sourceLang: 'zh',
|
14 |
+
targetLang: 'en',
|
15 |
+
transcript: '',
|
16 |
+
outputText: '',
|
17 |
+
isRecording: false,
|
18 |
+
sourceLanguages: [],
|
19 |
+
targetLanguages: [],
|
20 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
21 |
+
currentMode: '' // 'plugin-translate' or 'hf-bridge'
|
22 |
+
},
|
23 |
+
|
24 |
+
onLoad: function () {
|
25 |
+
this.initializeLanguages();
|
26 |
+
this.initRecorderManager();
|
27 |
+
},
|
28 |
+
|
29 |
+
// --- Language Selection Logic ---
|
30 |
+
initializeLanguages: function () {
|
31 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
32 |
+
this.setData({
|
33 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
34 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
35 |
+
});
|
36 |
+
},
|
37 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
38 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
39 |
+
swapLanguages: function() {
|
40 |
+
this.setData({
|
41 |
+
sourceLang: this.data.targetLang,
|
42 |
+
targetLang: this.data.sourceLang,
|
43 |
+
transcript: this.data.outputText,
|
44 |
+
outputText: this.data.transcript
|
45 |
+
}, this.initializeLanguages);
|
46 |
+
},
|
47 |
+
|
48 |
+
// --- Unified, Simplified, and Correct Recording Logic ---
|
49 |
+
initRecorderManager: function() {
|
50 |
+
manager.onStart = () => {
|
51 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
52 |
+
};
|
53 |
+
|
54 |
+
// CORRECTED: Only use onStop for final results to prevent race conditions.
|
55 |
+
manager.onStop = (res) => {
|
56 |
+
this.setData({ isRecording: false });
|
57 |
+
if (this.data.currentMode === 'plugin-translate') {
|
58 |
+
this.setData({ transcript: res.result, outputText: res.translateResult });
|
59 |
+
} else if (this.data.currentMode === 'hf-bridge') {
|
60 |
+
if (res.tempFilePath) {
|
61 |
+
this.uploadAudioForASR(res.tempFilePath);
|
62 |
+
} else {
|
63 |
+
this.setData({ transcript: '录音文件获取失败' });
|
64 |
+
}
|
65 |
+
}
|
66 |
+
};
|
67 |
+
|
68 |
+
manager.onError = (res) => {
|
69 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
70 |
+
};
|
71 |
+
},
|
72 |
+
|
73 |
+
startRecording: function () {
|
74 |
+
const { sourceLang, targetLang } = this.data;
|
75 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
76 |
+
|
77 |
+
this.setData({ isRecording: true });
|
78 |
+
|
79 |
+
if (isChineseEnglish) {
|
80 |
+
this.setData({ currentMode: 'plugin-translate' });
|
81 |
+
manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code });
|
82 |
+
} else {
|
83 |
+
this.setData({ currentMode: 'hf-bridge' });
|
84 |
+
manager.start({ lang: this.data.languages[sourceLang].code, translate: false });
|
85 |
+
}
|
86 |
+
},
|
87 |
+
|
88 |
+
stopRecording: function () {
|
89 |
+
manager.stop();
|
90 |
+
},
|
91 |
+
|
92 |
+
// --- HF Bridge Translation Flow ---
|
93 |
+
uploadAudioForASR: function (filePath) {
|
94 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
95 |
+
const fileSystemManager = wx.getFileSystemManager();
|
96 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
97 |
+
wx.request({
|
98 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
99 |
+
method: 'POST',
|
100 |
+
header: { 'Content-Type': 'application/json' },
|
101 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
102 |
+
timeout: 60000,
|
103 |
+
success: (asrRes) => {
|
104 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
105 |
+
const transcript = asrRes.data.transcript;
|
106 |
+
this.setData({ transcript });
|
107 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
108 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
109 |
+
},
|
110 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
111 |
+
});
|
112 |
+
}});
|
113 |
+
},
|
114 |
+
|
115 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
116 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
117 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
118 |
+
if (englishResult) {
|
119 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
120 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
121 |
+
if (finalResult) {
|
122 |
+
this.setData({ outputText: finalResult });
|
123 |
+
}
|
124 |
+
});
|
125 |
+
}
|
126 |
+
});
|
127 |
+
},
|
128 |
+
|
129 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
130 |
+
wx.request({
|
131 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
132 |
+
method: 'POST',
|
133 |
+
header: { 'Content-Type': 'application/json' },
|
134 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
135 |
+
timeout: 30000,
|
136 |
+
success: (res) => {
|
137 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
138 |
+
callback(res.data.translated_text);
|
139 |
+
} else {
|
140 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
141 |
+
callback(null);
|
142 |
+
}
|
143 |
+
},
|
144 |
+
fail: () => {
|
145 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
146 |
+
callback(null);
|
147 |
+
}
|
148 |
+
});
|
149 |
+
}
|
150 |
+
});
|
pages/index/index_v7_old.js
ADDED
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
|
3 |
+
Page({
|
4 |
+
data: {
|
5 |
+
languages: {
|
6 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
7 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' },
|
8 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' },
|
9 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' }
|
10 |
+
},
|
11 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
12 |
+
sourceLang: 'zh',
|
13 |
+
targetLang: 'en',
|
14 |
+
transcript: '',
|
15 |
+
outputText: '',
|
16 |
+
isRecording: false,
|
17 |
+
sourceLanguages: [],
|
18 |
+
targetLanguages: [],
|
19 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
20 |
+
currentMode: '' // 'plugin-translate' or 'hf-bridge'
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.initRecorderManager(); // Correctly initialize the manager within the page lifecycle
|
26 |
+
},
|
27 |
+
|
28 |
+
// --- Language Selection Logic ---
|
29 |
+
initializeLanguages: function () {
|
30 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
31 |
+
this.setData({
|
32 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
33 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
34 |
+
});
|
35 |
+
},
|
36 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
37 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
38 |
+
swapLanguages: function() {
|
39 |
+
this.setData({
|
40 |
+
sourceLang: this.data.targetLang,
|
41 |
+
targetLang: this.data.sourceLang,
|
42 |
+
transcript: this.data.outputText,
|
43 |
+
outputText: this.data.transcript
|
44 |
+
}, this.initializeLanguages);
|
45 |
+
},
|
46 |
+
|
47 |
+
// --- CORRECTED: Unified, Lifecycle-Aware Recording Logic ---
|
48 |
+
initRecorderManager: function() {
|
49 |
+
const manager = plugin.getRecordRecognitionManager();
|
50 |
+
|
51 |
+
manager.onStart = () => {
|
52 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
53 |
+
};
|
54 |
+
|
55 |
+
manager.onStop = (res) => {
|
56 |
+
this.setData({ isRecording: false });
|
57 |
+
if (this.data.currentMode === 'plugin-translate') {
|
58 |
+
if (res.result) {
|
59 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
60 |
+
} else {
|
61 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
62 |
+
}
|
63 |
+
} else if (this.data.currentMode === 'hf-bridge') {
|
64 |
+
if (res.tempFilePath) {
|
65 |
+
this.uploadAudioForASR(res.tempFilePath);
|
66 |
+
} else {
|
67 |
+
this.setData({ transcript: '录音文件获取失败' });
|
68 |
+
}
|
69 |
+
}
|
70 |
+
};
|
71 |
+
|
72 |
+
manager.onError = (res) => {
|
73 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
74 |
+
};
|
75 |
+
|
76 |
+
// CRITICAL: Save the manager instance to the page context
|
77 |
+
this.manager = manager;
|
78 |
+
},
|
79 |
+
|
80 |
+
startRecording: function () {
|
81 |
+
const { sourceLang, targetLang } = this.data;
|
82 |
+
const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh');
|
83 |
+
|
84 |
+
this.setData({ isRecording: true });
|
85 |
+
|
86 |
+
if (isChineseEnglish) {
|
87 |
+
this.setData({ currentMode: 'plugin-translate' });
|
88 |
+
this.manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code });
|
89 |
+
} else {
|
90 |
+
this.setData({ currentMode: 'hf-bridge' });
|
91 |
+
this.manager.start({ lang: this.data.languages[sourceLang].code, translate: false });
|
92 |
+
}
|
93 |
+
},
|
94 |
+
|
95 |
+
stopRecording: function () {
|
96 |
+
this.manager.stop();
|
97 |
+
},
|
98 |
+
|
99 |
+
// --- HF Bridge Translation Flow (This part remains the same) ---
|
100 |
+
uploadAudioForASR: function (filePath) {
|
101 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
102 |
+
const fileSystemManager = wx.getFileSystemManager();
|
103 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
104 |
+
wx.request({
|
105 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
106 |
+
method: 'POST',
|
107 |
+
header: { 'Content-Type': 'application/json' },
|
108 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
109 |
+
timeout: 60000,
|
110 |
+
success: (asrRes) => {
|
111 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
112 |
+
const transcript = asrRes.data.transcript;
|
113 |
+
this.setData({ transcript });
|
114 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
115 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
116 |
+
},
|
117 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
118 |
+
});
|
119 |
+
}});
|
120 |
+
},
|
121 |
+
|
122 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
123 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
124 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
125 |
+
if (englishResult) {
|
126 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
127 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
128 |
+
if (finalResult) {
|
129 |
+
this.setData({ outputText: finalResult });
|
130 |
+
}
|
131 |
+
});
|
132 |
+
}
|
133 |
+
});
|
134 |
+
},
|
135 |
+
|
136 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
137 |
+
wx.request({
|
138 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
139 |
+
method: 'POST',
|
140 |
+
header: { 'Content-Type': 'application/json' },
|
141 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
142 |
+
timeout: 30000,
|
143 |
+
success: (res) => {
|
144 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
145 |
+
callback(res.data.translated_text);
|
146 |
+
} else {
|
147 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
148 |
+
callback(null);
|
149 |
+
}
|
150 |
+
},
|
151 |
+
fail: () => {
|
152 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
153 |
+
callback(null);
|
154 |
+
}
|
155 |
+
});
|
156 |
+
}
|
157 |
+
});
|
pages/index/index_v8_old.js
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const recorderManager = wx.getRecorderManager();
|
2 |
+
|
3 |
+
Page({
|
4 |
+
data: {
|
5 |
+
languages: {
|
6 |
+
'zh': { name: '中文', flag: 'cn' },
|
7 |
+
'en': { name: 'English', flag: 'us' },
|
8 |
+
'ja': { name: '日本語', flag: 'jp' },
|
9 |
+
'ko': { name: '한국어', flag: 'kr' }
|
10 |
+
},
|
11 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
12 |
+
sourceLang: 'zh',
|
13 |
+
targetLang: 'en',
|
14 |
+
transcript: '',
|
15 |
+
outputText: '',
|
16 |
+
isRecording: false,
|
17 |
+
sourceLanguages: [],
|
18 |
+
targetLanguages: [],
|
19 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space'
|
20 |
+
},
|
21 |
+
|
22 |
+
onLoad: function () {
|
23 |
+
this.initializeLanguages();
|
24 |
+
this.initRecorderManager();
|
25 |
+
},
|
26 |
+
|
27 |
+
// --- Language Selection Logic ---
|
28 |
+
initializeLanguages: function () {
|
29 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
30 |
+
this.setData({
|
31 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
32 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
33 |
+
});
|
34 |
+
},
|
35 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
36 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
37 |
+
swapLanguages: function() {
|
38 |
+
this.setData({
|
39 |
+
sourceLang: this.data.targetLang,
|
40 |
+
targetLang: this.data.sourceLang,
|
41 |
+
transcript: this.data.outputText,
|
42 |
+
outputText: this.data.transcript
|
43 |
+
}, this.initializeLanguages);
|
44 |
+
},
|
45 |
+
|
46 |
+
// --- Simplified, Unified, and Correct Recording Logic ---
|
47 |
+
initRecorderManager: function() {
|
48 |
+
recorderManager.onStart = () => {
|
49 |
+
this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' });
|
50 |
+
};
|
51 |
+
|
52 |
+
recorderManager.onStop = (res) => {
|
53 |
+
this.setData({ isRecording: false });
|
54 |
+
if (res.tempFilePath) {
|
55 |
+
this.uploadAudioForASR(res.tempFilePath);
|
56 |
+
} else {
|
57 |
+
this.setData({ transcript: '录音文件获取失败' });
|
58 |
+
}
|
59 |
+
};
|
60 |
+
|
61 |
+
recorderManager.onError = (res) => {
|
62 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
63 |
+
};
|
64 |
+
},
|
65 |
+
|
66 |
+
startRecording: function () {
|
67 |
+
recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' });
|
68 |
+
},
|
69 |
+
|
70 |
+
stopRecording: function () {
|
71 |
+
recorderManager.stop();
|
72 |
+
},
|
73 |
+
|
74 |
+
// --- Unified Backend Flow ---
|
75 |
+
uploadAudioForASR: function (filePath) {
|
76 |
+
this.setData({ transcript: '正在识别 (1/3)...' });
|
77 |
+
const fileSystemManager = wx.getFileSystemManager();
|
78 |
+
fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => {
|
79 |
+
wx.request({
|
80 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
81 |
+
method: 'POST',
|
82 |
+
header: { 'Content-Type': 'application/json' },
|
83 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
84 |
+
timeout: 60000,
|
85 |
+
success: (asrRes) => {
|
86 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
87 |
+
const transcript = asrRes.data.transcript;
|
88 |
+
this.setData({ transcript });
|
89 |
+
// If source and target are the same, no need to translate
|
90 |
+
if (this.data.sourceLang === this.data.targetLang) {
|
91 |
+
this.setData({ outputText: transcript });
|
92 |
+
return;
|
93 |
+
}
|
94 |
+
this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
95 |
+
} else { this.setData({ transcript: 'HF识别失败' }); }
|
96 |
+
},
|
97 |
+
fail: () => { this.setData({ transcript: 'HF识别请求失败' }); }
|
98 |
+
});
|
99 |
+
}});
|
100 |
+
},
|
101 |
+
|
102 |
+
fullBackendBridge: function(text, sourceLang, targetLang) {
|
103 |
+
// If source is not English, bridge through English
|
104 |
+
if (sourceLang !== 'en') {
|
105 |
+
this.setData({ outputText: '翻译中 (2/3)..' });
|
106 |
+
this.translateViaHF(text, sourceLang, 'en', (englishResult) => {
|
107 |
+
if (englishResult) {
|
108 |
+
// If the final target was English, we are done
|
109 |
+
if (targetLang === 'en') {
|
110 |
+
this.setData({ outputText: englishResult });
|
111 |
+
} else {
|
112 |
+
this.setData({ outputText: '翻译中 (3/3)..' });
|
113 |
+
this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => {
|
114 |
+
if (finalResult) {
|
115 |
+
this.setData({ outputText: finalResult });
|
116 |
+
}
|
117 |
+
});
|
118 |
+
}
|
119 |
+
}
|
120 |
+
});
|
121 |
+
} else {
|
122 |
+
// If source is English, directly translate to target
|
123 |
+
this.setData({ outputText: '翻译中 (2/2)..' });
|
124 |
+
this.translateViaHF(text, sourceLang, targetLang, (finalResult) => {
|
125 |
+
if (finalResult) {
|
126 |
+
this.setData({ outputText: finalResult });
|
127 |
+
}
|
128 |
+
});
|
129 |
+
}
|
130 |
+
},
|
131 |
+
|
132 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
133 |
+
wx.request({
|
134 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
135 |
+
method: 'POST',
|
136 |
+
header: { 'Content-Type': 'application/json' },
|
137 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
138 |
+
timeout: 30000,
|
139 |
+
success: (res) => {
|
140 |
+
if (res.statusCode === 200 && res.data.translated_text) {
|
141 |
+
callback(res.data.translated_text);
|
142 |
+
} else {
|
143 |
+
this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` });
|
144 |
+
callback(null);
|
145 |
+
}
|
146 |
+
},
|
147 |
+
fail: () => {
|
148 |
+
this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` });
|
149 |
+
callback(null);
|
150 |
+
}
|
151 |
+
});
|
152 |
+
}
|
153 |
+
});
|
pages/index/index_v9_old.js
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin('WechatSI');
|
2 |
+
const manager = plugin.getRecordRecognitionManager();
|
3 |
+
|
4 |
+
Page({
|
5 |
+
data: {
|
6 |
+
languages: {
|
7 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN' },
|
8 |
+
'en': { name: 'English', flag: 'us', code: 'en_US' }
|
9 |
+
},
|
10 |
+
langCodes: ['zh', 'en'], // Only CN and EN for this test version
|
11 |
+
sourceLang: 'zh',
|
12 |
+
targetLang: 'en',
|
13 |
+
transcript: '',
|
14 |
+
outputText: '',
|
15 |
+
isRecording: false,
|
16 |
+
sourceLanguages: [],
|
17 |
+
targetLanguages: []
|
18 |
+
},
|
19 |
+
|
20 |
+
onLoad: function () {
|
21 |
+
this.initializeLanguages();
|
22 |
+
this.initRecorderManager();
|
23 |
+
},
|
24 |
+
|
25 |
+
// --- Language Selection Logic (Simplified for CN/EN only) ---
|
26 |
+
initializeLanguages: function () {
|
27 |
+
const { langCodes, languages, sourceLang, targetLang } = this.data;
|
28 |
+
this.setData({
|
29 |
+
sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })),
|
30 |
+
targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang }))
|
31 |
+
});
|
32 |
+
},
|
33 |
+
selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
34 |
+
selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
35 |
+
swapLanguages: function() {
|
36 |
+
this.setData({
|
37 |
+
sourceLang: this.data.targetLang,
|
38 |
+
targetLang: this.data.sourceLang,
|
39 |
+
transcript: this.data.outputText,
|
40 |
+
outputText: this.data.transcript
|
41 |
+
}, this.initializeLanguages);
|
42 |
+
},
|
43 |
+
|
44 |
+
// --- Recorder Manager Initialization (Pure Plugin Mode) ---
|
45 |
+
initRecorderManager: function() {
|
46 |
+
manager.onStart = () => {
|
47 |
+
this.setData({ transcript: '正在聆听...', outputText: '' });
|
48 |
+
};
|
49 |
+
|
50 |
+
// Only onStop is used for final results
|
51 |
+
manager.onStop = (res) => {
|
52 |
+
this.setData({ isRecording: false });
|
53 |
+
if (res.result) {
|
54 |
+
this.setData({ transcript: res.result, outputText: res.translateResult || '' });
|
55 |
+
} else {
|
56 |
+
this.setData({ transcript: '识别结果为空', outputText: '' });
|
57 |
+
}
|
58 |
+
};
|
59 |
+
|
60 |
+
manager.onError = (res) => {
|
61 |
+
this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg });
|
62 |
+
};
|
63 |
+
},
|
64 |
+
|
65 |
+
// --- Recording Start/Stop (Pure Plugin Mode) ---
|
66 |
+
startRecording: function () {
|
67 |
+
const { sourceLang, targetLang } = this.data;
|
68 |
+
this.setData({ isRecording: true });
|
69 |
+
manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code });
|
70 |
+
},
|
71 |
+
|
72 |
+
stopRecording: function () {
|
73 |
+
manager.stop();
|
74 |
+
}
|
75 |
+
});
|
pages/index/indexz_v23_old.js
ADDED
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const plugin = requirePlugin("WechatSI");
|
2 |
+
const pluginManager = plugin.getRecordRecognitionManager();
|
3 |
+
const nativeRecorderManager = wx.getRecorderManager();
|
4 |
+
|
5 |
+
Page({
|
6 |
+
data: {
|
7 |
+
languages: {
|
8 |
+
'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true },
|
9 |
+
'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true },
|
10 |
+
'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false },
|
11 |
+
'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false }
|
12 |
+
},
|
13 |
+
langCodes: ['zh', 'en', 'ja', 'ko'],
|
14 |
+
sourceLang: 'zh',
|
15 |
+
targetLang: 'en',
|
16 |
+
transcript: '',
|
17 |
+
outputText: '',
|
18 |
+
isRecording: false,
|
19 |
+
hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space',
|
20 |
+
currentManagerType: null // 'plugin' or 'native'
|
21 |
+
},
|
22 |
+
|
23 |
+
onLoad: function () {
|
24 |
+
this.initializeLanguages();
|
25 |
+
this.initPluginManager();
|
26 |
+
this.initNativeRecorderManager();
|
27 |
+
},
|
28 |
+
|
29 |
+
// --- Language Selection Logic (Simplified) ---
|
30 |
+
initializeLanguages: function () {
|
31 |
+
const { languages, sourceLang, targetLang } = this.data;
|
32 |
+
this.setData({
|
33 |
+
sourceLanguages: Object.keys(languages).map(key => ({ ...languages[key], langCode: key, selected: key === sourceLang })),
|
34 |
+
targetLanguages: Object.keys(languages).map(key => ({ ...languages[key], langCode: key, selected: key === targetLang }))
|
35 |
+
});
|
36 |
+
},
|
37 |
+
selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
38 |
+
selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); },
|
39 |
+
swapLanguages: function () {
|
40 |
+
this.setData({
|
41 |
+
sourceLang: this.data.targetLang,
|
42 |
+
targetLang: this.data.sourceLang,
|
43 |
+
transcript: this.data.outputText,
|
44 |
+
outputText: this.data.transcript
|
45 |
+
}, this.initializeLanguages);
|
46 |
+
},
|
47 |
+
|
48 |
+
// --- Manager Initializations with Detailed Error Logging ---
|
49 |
+
initPluginManager: function () {
|
50 |
+
pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' });
|
51 |
+
pluginManager.onRecognize = (res) => this.setData({ transcript: res.result });
|
52 |
+
pluginManager.onStop = (res) => {
|
53 |
+
this.setData({ isRecording: false });
|
54 |
+
if (!res.result) { return this.setData({ transcript: '识别结果为空' }); }
|
55 |
+
this.setData({ transcript: res.result });
|
56 |
+
const { sourceLang, targetLang } = this.data;
|
57 |
+
const isPluginTarget = this.data.languages[targetLang].usePlugin;
|
58 |
+
if (isPluginTarget) {
|
59 |
+
this.translateViaPlugin(res.result);
|
60 |
+
} else {
|
61 |
+
this.translateViaHfBridge(res.result, sourceLang, targetLang);
|
62 |
+
}
|
63 |
+
};
|
64 |
+
pluginManager.onError = (res) => this.setData({ isRecording: false, transcript: `插件错误: ${res.msg}` });
|
65 |
+
},
|
66 |
+
|
67 |
+
initNativeRecorderManager: function () {
|
68 |
+
nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' });
|
69 |
+
nativeRecorderManager.onStop = (res) => {
|
70 |
+
this.setData({ isRecording: false });
|
71 |
+
if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); }
|
72 |
+
else { this.setData({ transcript: '原生录音文件获取失败' }); }
|
73 |
+
};
|
74 |
+
nativeRecorderManager.onError = (res) => this.setData({ isRecording: false, transcript: `原生录音失败: ${res.errMsg}` });
|
75 |
+
},
|
76 |
+
|
77 |
+
// --- Main Record Button Handler ---
|
78 |
+
handleRecordToggle: function() {
|
79 |
+
if (this.data.isRecording) {
|
80 |
+
this.stopRecording();
|
81 |
+
return;
|
82 |
+
}
|
83 |
+
// Check permissions before starting a new recording
|
84 |
+
wx.getSetting({
|
85 |
+
success: (res) => {
|
86 |
+
if (!res.authSetting['scope.record']) {
|
87 |
+
wx.authorize({
|
88 |
+
scope: 'scope.record',
|
89 |
+
success: this.startRecording,
|
90 |
+
fail: () => wx.showToast({ title: '您拒绝了录音权限', icon: 'none' })
|
91 |
+
});
|
92 |
+
} else {
|
93 |
+
this.startRecording();
|
94 |
+
}
|
95 |
+
},
|
96 |
+
fail: () => wx.showToast({ title: '无法获取权限设置', icon: 'none' })
|
97 |
+
});
|
98 |
+
},
|
99 |
+
|
100 |
+
startRecording: function () {
|
101 |
+
const { sourceLang, languages } = this.data;
|
102 |
+
const shouldUsePlugin = languages[sourceLang].usePlugin;
|
103 |
+
const managerType = shouldUsePlugin ? 'plugin' : 'native';
|
104 |
+
|
105 |
+
this.setData({ isRecording: true, currentManagerType: managerType });
|
106 |
+
|
107 |
+
if (shouldUsePlugin) {
|
108 |
+
pluginManager.start({ lang: languages[sourceLang].code, translate: false });
|
109 |
+
} else {
|
110 |
+
nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 });
|
111 |
+
}
|
112 |
+
},
|
113 |
+
|
114 |
+
stopRecording: function () {
|
115 |
+
if (this.data.currentManagerType === 'native') {
|
116 |
+
nativeRecorderManager.stop();
|
117 |
+
} else {
|
118 |
+
pluginManager.stop();
|
119 |
+
}
|
120 |
+
},
|
121 |
+
|
122 |
+
// --- Translation Logic ---
|
123 |
+
translateViaPlugin: function(text) {
|
124 |
+
const { sourceLang, targetLang, languages } = this.data;
|
125 |
+
if (sourceLang === targetLang) { return this.setData({ outputText: text }); }
|
126 |
+
this.setData({ outputText: '翻译中 (插件)...' });
|
127 |
+
plugin.translate({
|
128 |
+
lfrom: languages[sourceLang].code,
|
129 |
+
lto: languages[targetLang].code,
|
130 |
+
content: text,
|
131 |
+
success: (res) => {
|
132 |
+
if (res.retcode === 0) { this.setData({ outputText: res.result }); }
|
133 |
+
else { this.setData({ outputText: `插件翻译失败: ${res.retcode}` }); }
|
134 |
+
},
|
135 |
+
fail: (err) => this.setData({ outputText: '插件翻译接口调用失败' })
|
136 |
+
});
|
137 |
+
},
|
138 |
+
|
139 |
+
uploadAudioForASR: function (filePath) {
|
140 |
+
this.setData({ transcript: '正在识别 (HF)...' });
|
141 |
+
wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => {
|
142 |
+
wx.request({
|
143 |
+
url: `${this.data.hfSpaceUrl}/api/asr`,
|
144 |
+
method: 'POST',
|
145 |
+
data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` },
|
146 |
+
timeout: 60000,
|
147 |
+
success: (asrRes) => {
|
148 |
+
if (asrRes.statusCode === 200 && asrRes.data.transcript) {
|
149 |
+
const transcript = asrRes.data.transcript;
|
150 |
+
this.setData({ transcript });
|
151 |
+
this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang);
|
152 |
+
} else { this.setData({ transcript: 'HF识别失败', outputText: asrRes.data.error || '' }); }
|
153 |
+
},
|
154 |
+
fail: () => this.setData({ transcript: 'HF识别请求失败' })
|
155 |
+
});
|
156 |
+
}});
|
157 |
+
},
|
158 |
+
|
159 |
+
translateViaHfBridge: function(text, source, target) {
|
160 |
+
if (source === target) { return this.setData({ outputText: text }); }
|
161 |
+
this.setData({ outputText: '翻译中 (HF)...' });
|
162 |
+
this.translateViaHF(text, source, target, (result) => {
|
163 |
+
if (result) { this.setData({ outputText: result }); }
|
164 |
+
});
|
165 |
+
},
|
166 |
+
|
167 |
+
translateViaHF: function(text, sourceLang, targetLang, callback) {
|
168 |
+
wx.request({
|
169 |
+
url: `${this.data.hfSpaceUrl}/api/translate`,
|
170 |
+
method: 'POST',
|
171 |
+
data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang },
|
172 |
+
timeout: 45000,
|
173 |
+
success: (res) => {
|
174 |
+
if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); }
|
175 |
+
else { this.setData({ outputText: `HF翻译失败: ${res.data.error || '未知错误'}` }); callback(null); }
|
176 |
+
},
|
177 |
+
fail: () => { this.setData({ outputText: 'HF翻译请求失败' }); callback(null); }
|
178 |
+
});
|
179 |
+
}
|
180 |
+
});
|
project.config.json
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"miniprogramRoot": "./",
|
3 |
+
"projectname": "wechat-miniprogram-translator",
|
4 |
+
"description": "实时翻译工具微信小程序版",
|
5 |
+
"appid": "wx4bf4c3f1ca6b34b0",
|
6 |
+
"setting": {
|
7 |
+
"urlCheck": true,
|
8 |
+
"es6": true,
|
9 |
+
"enhance": true,
|
10 |
+
"postcss": true,
|
11 |
+
"minified": true,
|
12 |
+
"newFeature": true,
|
13 |
+
"coverView": true,
|
14 |
+
"nodeModules": true,
|
15 |
+
"autoAudits": false,
|
16 |
+
"showShadowRootInWxmlPanel": true,
|
17 |
+
"scopeDataCheck": false,
|
18 |
+
"checkSiteMap": true,
|
19 |
+
"enableEngineNative": false,
|
20 |
+
"useMultiFrameRuntime": true,
|
21 |
+
"useApiHook": true,
|
22 |
+
"useApiHostProcess": true,
|
23 |
+
"babelSetting": {
|
24 |
+
"ignore": [],
|
25 |
+
"disablePlugins": [],
|
26 |
+
"outputPath": ""
|
27 |
+
},
|
28 |
+
"condition": {
|
29 |
+
"search": {
|
30 |
+
"list": []
|
31 |
+
},
|
32 |
+
"conversation": {
|
33 |
+
"list": []
|
34 |
+
},
|
35 |
+
"plugin": {
|
36 |
+
"list": []
|
37 |
+
},
|
38 |
+
"sitemap": {
|
39 |
+
"list": []
|
40 |
+
},
|
41 |
+
"enableTools": true
|
42 |
+
},
|
43 |
+
"compileWorklet": false,
|
44 |
+
"uglifyFileName": false,
|
45 |
+
"uploadWithSourceMap": true,
|
46 |
+
"packNpmManually": false,
|
47 |
+
"packNpmRelationList": [],
|
48 |
+
"minifyWXSS": true,
|
49 |
+
"minifyWXML": true,
|
50 |
+
"localPlugins": false,
|
51 |
+
"disableUseStrict": false,
|
52 |
+
"useCompilerPlugins": false,
|
53 |
+
"swc": false,
|
54 |
+
"disableSWC": true
|
55 |
+
},
|
56 |
+
"compileType": "miniprogram",
|
57 |
+
"libVersion": "3.8.10",
|
58 |
+
"srcMiniprogramRoot": "",
|
59 |
+
"editorSetting": {
|
60 |
+
"tabIndent": "tab",
|
61 |
+
"tabSize": 4
|
62 |
+
},
|
63 |
+
"simulatorPluginLibVersion": {},
|
64 |
+
"packOptions": {
|
65 |
+
"ignore": [],
|
66 |
+
"include": []
|
67 |
+
}
|
68 |
+
}
|
project.private.config.json
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"libVersion": "3.8.10",
|
3 |
+
"projectname": "%E4%BD%A0%E8%AF%B4%E6%88%91%E8%AF%91project-hf",
|
4 |
+
"setting": {
|
5 |
+
"urlCheck": false,
|
6 |
+
"coverView": true,
|
7 |
+
"lazyloadPlaceholderEnable": false,
|
8 |
+
"skylineRenderEnable": false,
|
9 |
+
"preloadBackgroundData": false,
|
10 |
+
"autoAudits": false,
|
11 |
+
"useApiHook": true,
|
12 |
+
"useApiHostProcess": true,
|
13 |
+
"showShadowRootInWxmlPanel": true,
|
14 |
+
"useStaticServer": false,
|
15 |
+
"useLanDebug": false,
|
16 |
+
"showES6CompileOption": false,
|
17 |
+
"compileHotReLoad": true,
|
18 |
+
"checkInvalidKey": true,
|
19 |
+
"ignoreDevUnusedFiles": true,
|
20 |
+
"bigPackageSizeSupport": false
|
21 |
+
}
|
22 |
+
}
|
requirements.txt
CHANGED
@@ -1,11 +1,10 @@
|
|
1 |
transformers
|
2 |
torch
|
3 |
sentencepiece
|
4 |
-
gradio
|
5 |
soundfile
|
6 |
sacremoses
|
7 |
fastapi
|
8 |
uvicorn
|
9 |
deepl
|
10 |
python-dotenv
|
11 |
-
optimum[onnxruntime]
|
|
|
1 |
transformers
|
2 |
torch
|
3 |
sentencepiece
|
|
|
4 |
soundfile
|
5 |
sacremoses
|
6 |
fastapi
|
7 |
uvicorn
|
8 |
deepl
|
9 |
python-dotenv
|
10 |
+
optimum[onnxruntime]
|
server/package-lock.json
ADDED
@@ -0,0 +1,917 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "server",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"lockfileVersion": 3,
|
5 |
+
"requires": true,
|
6 |
+
"packages": {
|
7 |
+
"": {
|
8 |
+
"name": "server",
|
9 |
+
"version": "1.0.0",
|
10 |
+
"license": "ISC",
|
11 |
+
"dependencies": {
|
12 |
+
"express": "^5.1.0",
|
13 |
+
"node-fetch": "^3.3.2"
|
14 |
+
}
|
15 |
+
},
|
16 |
+
"node_modules/accepts": {
|
17 |
+
"version": "2.0.0",
|
18 |
+
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
19 |
+
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
20 |
+
"license": "MIT",
|
21 |
+
"dependencies": {
|
22 |
+
"mime-types": "^3.0.0",
|
23 |
+
"negotiator": "^1.0.0"
|
24 |
+
},
|
25 |
+
"engines": {
|
26 |
+
"node": ">= 0.6"
|
27 |
+
}
|
28 |
+
},
|
29 |
+
"node_modules/body-parser": {
|
30 |
+
"version": "2.2.0",
|
31 |
+
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
32 |
+
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
33 |
+
"license": "MIT",
|
34 |
+
"dependencies": {
|
35 |
+
"bytes": "^3.1.2",
|
36 |
+
"content-type": "^1.0.5",
|
37 |
+
"debug": "^4.4.0",
|
38 |
+
"http-errors": "^2.0.0",
|
39 |
+
"iconv-lite": "^0.6.3",
|
40 |
+
"on-finished": "^2.4.1",
|
41 |
+
"qs": "^6.14.0",
|
42 |
+
"raw-body": "^3.0.0",
|
43 |
+
"type-is": "^2.0.0"
|
44 |
+
},
|
45 |
+
"engines": {
|
46 |
+
"node": ">=18"
|
47 |
+
}
|
48 |
+
},
|
49 |
+
"node_modules/bytes": {
|
50 |
+
"version": "3.1.2",
|
51 |
+
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
52 |
+
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
53 |
+
"license": "MIT",
|
54 |
+
"engines": {
|
55 |
+
"node": ">= 0.8"
|
56 |
+
}
|
57 |
+
},
|
58 |
+
"node_modules/call-bind-apply-helpers": {
|
59 |
+
"version": "1.0.2",
|
60 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
61 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
62 |
+
"license": "MIT",
|
63 |
+
"dependencies": {
|
64 |
+
"es-errors": "^1.3.0",
|
65 |
+
"function-bind": "^1.1.2"
|
66 |
+
},
|
67 |
+
"engines": {
|
68 |
+
"node": ">= 0.4"
|
69 |
+
}
|
70 |
+
},
|
71 |
+
"node_modules/call-bound": {
|
72 |
+
"version": "1.0.4",
|
73 |
+
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
74 |
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
75 |
+
"license": "MIT",
|
76 |
+
"dependencies": {
|
77 |
+
"call-bind-apply-helpers": "^1.0.2",
|
78 |
+
"get-intrinsic": "^1.3.0"
|
79 |
+
},
|
80 |
+
"engines": {
|
81 |
+
"node": ">= 0.4"
|
82 |
+
},
|
83 |
+
"funding": {
|
84 |
+
"url": "https://github.com/sponsors/ljharb"
|
85 |
+
}
|
86 |
+
},
|
87 |
+
"node_modules/content-disposition": {
|
88 |
+
"version": "1.0.0",
|
89 |
+
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
90 |
+
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
91 |
+
"license": "MIT",
|
92 |
+
"dependencies": {
|
93 |
+
"safe-buffer": "5.2.1"
|
94 |
+
},
|
95 |
+
"engines": {
|
96 |
+
"node": ">= 0.6"
|
97 |
+
}
|
98 |
+
},
|
99 |
+
"node_modules/content-type": {
|
100 |
+
"version": "1.0.5",
|
101 |
+
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
102 |
+
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
103 |
+
"license": "MIT",
|
104 |
+
"engines": {
|
105 |
+
"node": ">= 0.6"
|
106 |
+
}
|
107 |
+
},
|
108 |
+
"node_modules/cookie": {
|
109 |
+
"version": "0.7.2",
|
110 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
111 |
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
112 |
+
"license": "MIT",
|
113 |
+
"engines": {
|
114 |
+
"node": ">= 0.6"
|
115 |
+
}
|
116 |
+
},
|
117 |
+
"node_modules/cookie-signature": {
|
118 |
+
"version": "1.2.2",
|
119 |
+
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
120 |
+
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
121 |
+
"license": "MIT",
|
122 |
+
"engines": {
|
123 |
+
"node": ">=6.6.0"
|
124 |
+
}
|
125 |
+
},
|
126 |
+
"node_modules/data-uri-to-buffer": {
|
127 |
+
"version": "4.0.1",
|
128 |
+
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
129 |
+
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
130 |
+
"license": "MIT",
|
131 |
+
"engines": {
|
132 |
+
"node": ">= 12"
|
133 |
+
}
|
134 |
+
},
|
135 |
+
"node_modules/debug": {
|
136 |
+
"version": "4.4.1",
|
137 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
138 |
+
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
139 |
+
"license": "MIT",
|
140 |
+
"dependencies": {
|
141 |
+
"ms": "^2.1.3"
|
142 |
+
},
|
143 |
+
"engines": {
|
144 |
+
"node": ">=6.0"
|
145 |
+
},
|
146 |
+
"peerDependenciesMeta": {
|
147 |
+
"supports-color": {
|
148 |
+
"optional": true
|
149 |
+
}
|
150 |
+
}
|
151 |
+
},
|
152 |
+
"node_modules/depd": {
|
153 |
+
"version": "2.0.0",
|
154 |
+
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
155 |
+
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
156 |
+
"license": "MIT",
|
157 |
+
"engines": {
|
158 |
+
"node": ">= 0.8"
|
159 |
+
}
|
160 |
+
},
|
161 |
+
"node_modules/dunder-proto": {
|
162 |
+
"version": "1.0.1",
|
163 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
164 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
165 |
+
"license": "MIT",
|
166 |
+
"dependencies": {
|
167 |
+
"call-bind-apply-helpers": "^1.0.1",
|
168 |
+
"es-errors": "^1.3.0",
|
169 |
+
"gopd": "^1.2.0"
|
170 |
+
},
|
171 |
+
"engines": {
|
172 |
+
"node": ">= 0.4"
|
173 |
+
}
|
174 |
+
},
|
175 |
+
"node_modules/ee-first": {
|
176 |
+
"version": "1.1.1",
|
177 |
+
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
178 |
+
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
179 |
+
"license": "MIT"
|
180 |
+
},
|
181 |
+
"node_modules/encodeurl": {
|
182 |
+
"version": "2.0.0",
|
183 |
+
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
184 |
+
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
185 |
+
"license": "MIT",
|
186 |
+
"engines": {
|
187 |
+
"node": ">= 0.8"
|
188 |
+
}
|
189 |
+
},
|
190 |
+
"node_modules/es-define-property": {
|
191 |
+
"version": "1.0.1",
|
192 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
193 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
194 |
+
"license": "MIT",
|
195 |
+
"engines": {
|
196 |
+
"node": ">= 0.4"
|
197 |
+
}
|
198 |
+
},
|
199 |
+
"node_modules/es-errors": {
|
200 |
+
"version": "1.3.0",
|
201 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
202 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
203 |
+
"license": "MIT",
|
204 |
+
"engines": {
|
205 |
+
"node": ">= 0.4"
|
206 |
+
}
|
207 |
+
},
|
208 |
+
"node_modules/es-object-atoms": {
|
209 |
+
"version": "1.1.1",
|
210 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
211 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
212 |
+
"license": "MIT",
|
213 |
+
"dependencies": {
|
214 |
+
"es-errors": "^1.3.0"
|
215 |
+
},
|
216 |
+
"engines": {
|
217 |
+
"node": ">= 0.4"
|
218 |
+
}
|
219 |
+
},
|
220 |
+
"node_modules/escape-html": {
|
221 |
+
"version": "1.0.3",
|
222 |
+
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
223 |
+
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
224 |
+
"license": "MIT"
|
225 |
+
},
|
226 |
+
"node_modules/etag": {
|
227 |
+
"version": "1.8.1",
|
228 |
+
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
229 |
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
230 |
+
"license": "MIT",
|
231 |
+
"engines": {
|
232 |
+
"node": ">= 0.6"
|
233 |
+
}
|
234 |
+
},
|
235 |
+
"node_modules/express": {
|
236 |
+
"version": "5.1.0",
|
237 |
+
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
238 |
+
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
239 |
+
"license": "MIT",
|
240 |
+
"dependencies": {
|
241 |
+
"accepts": "^2.0.0",
|
242 |
+
"body-parser": "^2.2.0",
|
243 |
+
"content-disposition": "^1.0.0",
|
244 |
+
"content-type": "^1.0.5",
|
245 |
+
"cookie": "^0.7.1",
|
246 |
+
"cookie-signature": "^1.2.1",
|
247 |
+
"debug": "^4.4.0",
|
248 |
+
"encodeurl": "^2.0.0",
|
249 |
+
"escape-html": "^1.0.3",
|
250 |
+
"etag": "^1.8.1",
|
251 |
+
"finalhandler": "^2.1.0",
|
252 |
+
"fresh": "^2.0.0",
|
253 |
+
"http-errors": "^2.0.0",
|
254 |
+
"merge-descriptors": "^2.0.0",
|
255 |
+
"mime-types": "^3.0.0",
|
256 |
+
"on-finished": "^2.4.1",
|
257 |
+
"once": "^1.4.0",
|
258 |
+
"parseurl": "^1.3.3",
|
259 |
+
"proxy-addr": "^2.0.7",
|
260 |
+
"qs": "^6.14.0",
|
261 |
+
"range-parser": "^1.2.1",
|
262 |
+
"router": "^2.2.0",
|
263 |
+
"send": "^1.1.0",
|
264 |
+
"serve-static": "^2.2.0",
|
265 |
+
"statuses": "^2.0.1",
|
266 |
+
"type-is": "^2.0.1",
|
267 |
+
"vary": "^1.1.2"
|
268 |
+
},
|
269 |
+
"engines": {
|
270 |
+
"node": ">= 18"
|
271 |
+
},
|
272 |
+
"funding": {
|
273 |
+
"type": "opencollective",
|
274 |
+
"url": "https://opencollective.com/express"
|
275 |
+
}
|
276 |
+
},
|
277 |
+
"node_modules/fetch-blob": {
|
278 |
+
"version": "3.2.0",
|
279 |
+
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
280 |
+
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
281 |
+
"funding": [
|
282 |
+
{
|
283 |
+
"type": "github",
|
284 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
285 |
+
},
|
286 |
+
{
|
287 |
+
"type": "paypal",
|
288 |
+
"url": "https://paypal.me/jimmywarting"
|
289 |
+
}
|
290 |
+
],
|
291 |
+
"license": "MIT",
|
292 |
+
"dependencies": {
|
293 |
+
"node-domexception": "^1.0.0",
|
294 |
+
"web-streams-polyfill": "^3.0.3"
|
295 |
+
},
|
296 |
+
"engines": {
|
297 |
+
"node": "^12.20 || >= 14.13"
|
298 |
+
}
|
299 |
+
},
|
300 |
+
"node_modules/finalhandler": {
|
301 |
+
"version": "2.1.0",
|
302 |
+
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
303 |
+
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
304 |
+
"license": "MIT",
|
305 |
+
"dependencies": {
|
306 |
+
"debug": "^4.4.0",
|
307 |
+
"encodeurl": "^2.0.0",
|
308 |
+
"escape-html": "^1.0.3",
|
309 |
+
"on-finished": "^2.4.1",
|
310 |
+
"parseurl": "^1.3.3",
|
311 |
+
"statuses": "^2.0.1"
|
312 |
+
},
|
313 |
+
"engines": {
|
314 |
+
"node": ">= 0.8"
|
315 |
+
}
|
316 |
+
},
|
317 |
+
"node_modules/formdata-polyfill": {
|
318 |
+
"version": "4.0.10",
|
319 |
+
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
320 |
+
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
321 |
+
"license": "MIT",
|
322 |
+
"dependencies": {
|
323 |
+
"fetch-blob": "^3.1.2"
|
324 |
+
},
|
325 |
+
"engines": {
|
326 |
+
"node": ">=12.20.0"
|
327 |
+
}
|
328 |
+
},
|
329 |
+
"node_modules/forwarded": {
|
330 |
+
"version": "0.2.0",
|
331 |
+
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
332 |
+
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
333 |
+
"license": "MIT",
|
334 |
+
"engines": {
|
335 |
+
"node": ">= 0.6"
|
336 |
+
}
|
337 |
+
},
|
338 |
+
"node_modules/fresh": {
|
339 |
+
"version": "2.0.0",
|
340 |
+
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
341 |
+
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
342 |
+
"license": "MIT",
|
343 |
+
"engines": {
|
344 |
+
"node": ">= 0.8"
|
345 |
+
}
|
346 |
+
},
|
347 |
+
"node_modules/function-bind": {
|
348 |
+
"version": "1.1.2",
|
349 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
350 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
351 |
+
"license": "MIT",
|
352 |
+
"funding": {
|
353 |
+
"url": "https://github.com/sponsors/ljharb"
|
354 |
+
}
|
355 |
+
},
|
356 |
+
"node_modules/get-intrinsic": {
|
357 |
+
"version": "1.3.0",
|
358 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
359 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
360 |
+
"license": "MIT",
|
361 |
+
"dependencies": {
|
362 |
+
"call-bind-apply-helpers": "^1.0.2",
|
363 |
+
"es-define-property": "^1.0.1",
|
364 |
+
"es-errors": "^1.3.0",
|
365 |
+
"es-object-atoms": "^1.1.1",
|
366 |
+
"function-bind": "^1.1.2",
|
367 |
+
"get-proto": "^1.0.1",
|
368 |
+
"gopd": "^1.2.0",
|
369 |
+
"has-symbols": "^1.1.0",
|
370 |
+
"hasown": "^2.0.2",
|
371 |
+
"math-intrinsics": "^1.1.0"
|
372 |
+
},
|
373 |
+
"engines": {
|
374 |
+
"node": ">= 0.4"
|
375 |
+
},
|
376 |
+
"funding": {
|
377 |
+
"url": "https://github.com/sponsors/ljharb"
|
378 |
+
}
|
379 |
+
},
|
380 |
+
"node_modules/get-proto": {
|
381 |
+
"version": "1.0.1",
|
382 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
383 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
384 |
+
"license": "MIT",
|
385 |
+
"dependencies": {
|
386 |
+
"dunder-proto": "^1.0.1",
|
387 |
+
"es-object-atoms": "^1.0.0"
|
388 |
+
},
|
389 |
+
"engines": {
|
390 |
+
"node": ">= 0.4"
|
391 |
+
}
|
392 |
+
},
|
393 |
+
"node_modules/gopd": {
|
394 |
+
"version": "1.2.0",
|
395 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
396 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
397 |
+
"license": "MIT",
|
398 |
+
"engines": {
|
399 |
+
"node": ">= 0.4"
|
400 |
+
},
|
401 |
+
"funding": {
|
402 |
+
"url": "https://github.com/sponsors/ljharb"
|
403 |
+
}
|
404 |
+
},
|
405 |
+
"node_modules/has-symbols": {
|
406 |
+
"version": "1.1.0",
|
407 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
408 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
409 |
+
"license": "MIT",
|
410 |
+
"engines": {
|
411 |
+
"node": ">= 0.4"
|
412 |
+
},
|
413 |
+
"funding": {
|
414 |
+
"url": "https://github.com/sponsors/ljharb"
|
415 |
+
}
|
416 |
+
},
|
417 |
+
"node_modules/hasown": {
|
418 |
+
"version": "2.0.2",
|
419 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
420 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
421 |
+
"license": "MIT",
|
422 |
+
"dependencies": {
|
423 |
+
"function-bind": "^1.1.2"
|
424 |
+
},
|
425 |
+
"engines": {
|
426 |
+
"node": ">= 0.4"
|
427 |
+
}
|
428 |
+
},
|
429 |
+
"node_modules/http-errors": {
|
430 |
+
"version": "2.0.0",
|
431 |
+
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
432 |
+
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
433 |
+
"license": "MIT",
|
434 |
+
"dependencies": {
|
435 |
+
"depd": "2.0.0",
|
436 |
+
"inherits": "2.0.4",
|
437 |
+
"setprototypeof": "1.2.0",
|
438 |
+
"statuses": "2.0.1",
|
439 |
+
"toidentifier": "1.0.1"
|
440 |
+
},
|
441 |
+
"engines": {
|
442 |
+
"node": ">= 0.8"
|
443 |
+
}
|
444 |
+
},
|
445 |
+
"node_modules/http-errors/node_modules/statuses": {
|
446 |
+
"version": "2.0.1",
|
447 |
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
448 |
+
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
449 |
+
"license": "MIT",
|
450 |
+
"engines": {
|
451 |
+
"node": ">= 0.8"
|
452 |
+
}
|
453 |
+
},
|
454 |
+
"node_modules/iconv-lite": {
|
455 |
+
"version": "0.6.3",
|
456 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
457 |
+
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
458 |
+
"license": "MIT",
|
459 |
+
"dependencies": {
|
460 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
461 |
+
},
|
462 |
+
"engines": {
|
463 |
+
"node": ">=0.10.0"
|
464 |
+
}
|
465 |
+
},
|
466 |
+
"node_modules/inherits": {
|
467 |
+
"version": "2.0.4",
|
468 |
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
469 |
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
470 |
+
"license": "ISC"
|
471 |
+
},
|
472 |
+
"node_modules/ipaddr.js": {
|
473 |
+
"version": "1.9.1",
|
474 |
+
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
475 |
+
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
476 |
+
"license": "MIT",
|
477 |
+
"engines": {
|
478 |
+
"node": ">= 0.10"
|
479 |
+
}
|
480 |
+
},
|
481 |
+
"node_modules/is-promise": {
|
482 |
+
"version": "4.0.0",
|
483 |
+
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
484 |
+
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
485 |
+
"license": "MIT"
|
486 |
+
},
|
487 |
+
"node_modules/math-intrinsics": {
|
488 |
+
"version": "1.1.0",
|
489 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
490 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
491 |
+
"license": "MIT",
|
492 |
+
"engines": {
|
493 |
+
"node": ">= 0.4"
|
494 |
+
}
|
495 |
+
},
|
496 |
+
"node_modules/media-typer": {
|
497 |
+
"version": "1.1.0",
|
498 |
+
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
499 |
+
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
500 |
+
"license": "MIT",
|
501 |
+
"engines": {
|
502 |
+
"node": ">= 0.8"
|
503 |
+
}
|
504 |
+
},
|
505 |
+
"node_modules/merge-descriptors": {
|
506 |
+
"version": "2.0.0",
|
507 |
+
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
508 |
+
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
509 |
+
"license": "MIT",
|
510 |
+
"engines": {
|
511 |
+
"node": ">=18"
|
512 |
+
},
|
513 |
+
"funding": {
|
514 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
515 |
+
}
|
516 |
+
},
|
517 |
+
"node_modules/mime-db": {
|
518 |
+
"version": "1.54.0",
|
519 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
520 |
+
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
521 |
+
"license": "MIT",
|
522 |
+
"engines": {
|
523 |
+
"node": ">= 0.6"
|
524 |
+
}
|
525 |
+
},
|
526 |
+
"node_modules/mime-types": {
|
527 |
+
"version": "3.0.1",
|
528 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
529 |
+
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
530 |
+
"license": "MIT",
|
531 |
+
"dependencies": {
|
532 |
+
"mime-db": "^1.54.0"
|
533 |
+
},
|
534 |
+
"engines": {
|
535 |
+
"node": ">= 0.6"
|
536 |
+
}
|
537 |
+
},
|
538 |
+
"node_modules/ms": {
|
539 |
+
"version": "2.1.3",
|
540 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
541 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
542 |
+
"license": "MIT"
|
543 |
+
},
|
544 |
+
"node_modules/negotiator": {
|
545 |
+
"version": "1.0.0",
|
546 |
+
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
547 |
+
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
548 |
+
"license": "MIT",
|
549 |
+
"engines": {
|
550 |
+
"node": ">= 0.6"
|
551 |
+
}
|
552 |
+
},
|
553 |
+
"node_modules/node-domexception": {
|
554 |
+
"version": "1.0.0",
|
555 |
+
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
556 |
+
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
557 |
+
"deprecated": "Use your platform's native DOMException instead",
|
558 |
+
"funding": [
|
559 |
+
{
|
560 |
+
"type": "github",
|
561 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
562 |
+
},
|
563 |
+
{
|
564 |
+
"type": "github",
|
565 |
+
"url": "https://paypal.me/jimmywarting"
|
566 |
+
}
|
567 |
+
],
|
568 |
+
"license": "MIT",
|
569 |
+
"engines": {
|
570 |
+
"node": ">=10.5.0"
|
571 |
+
}
|
572 |
+
},
|
573 |
+
"node_modules/node-fetch": {
|
574 |
+
"version": "3.3.2",
|
575 |
+
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
576 |
+
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
577 |
+
"license": "MIT",
|
578 |
+
"dependencies": {
|
579 |
+
"data-uri-to-buffer": "^4.0.0",
|
580 |
+
"fetch-blob": "^3.1.4",
|
581 |
+
"formdata-polyfill": "^4.0.10"
|
582 |
+
},
|
583 |
+
"engines": {
|
584 |
+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
585 |
+
},
|
586 |
+
"funding": {
|
587 |
+
"type": "opencollective",
|
588 |
+
"url": "https://opencollective.com/node-fetch"
|
589 |
+
}
|
590 |
+
},
|
591 |
+
"node_modules/object-inspect": {
|
592 |
+
"version": "1.13.4",
|
593 |
+
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
594 |
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
595 |
+
"license": "MIT",
|
596 |
+
"engines": {
|
597 |
+
"node": ">= 0.4"
|
598 |
+
},
|
599 |
+
"funding": {
|
600 |
+
"url": "https://github.com/sponsors/ljharb"
|
601 |
+
}
|
602 |
+
},
|
603 |
+
"node_modules/on-finished": {
|
604 |
+
"version": "2.4.1",
|
605 |
+
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
606 |
+
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
607 |
+
"license": "MIT",
|
608 |
+
"dependencies": {
|
609 |
+
"ee-first": "1.1.1"
|
610 |
+
},
|
611 |
+
"engines": {
|
612 |
+
"node": ">= 0.8"
|
613 |
+
}
|
614 |
+
},
|
615 |
+
"node_modules/once": {
|
616 |
+
"version": "1.4.0",
|
617 |
+
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
618 |
+
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
619 |
+
"license": "ISC",
|
620 |
+
"dependencies": {
|
621 |
+
"wrappy": "1"
|
622 |
+
}
|
623 |
+
},
|
624 |
+
"node_modules/parseurl": {
|
625 |
+
"version": "1.3.3",
|
626 |
+
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
627 |
+
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
628 |
+
"license": "MIT",
|
629 |
+
"engines": {
|
630 |
+
"node": ">= 0.8"
|
631 |
+
}
|
632 |
+
},
|
633 |
+
"node_modules/path-to-regexp": {
|
634 |
+
"version": "8.2.0",
|
635 |
+
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
636 |
+
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
|
637 |
+
"license": "MIT",
|
638 |
+
"engines": {
|
639 |
+
"node": ">=16"
|
640 |
+
}
|
641 |
+
},
|
642 |
+
"node_modules/proxy-addr": {
|
643 |
+
"version": "2.0.7",
|
644 |
+
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
645 |
+
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
646 |
+
"license": "MIT",
|
647 |
+
"dependencies": {
|
648 |
+
"forwarded": "0.2.0",
|
649 |
+
"ipaddr.js": "1.9.1"
|
650 |
+
},
|
651 |
+
"engines": {
|
652 |
+
"node": ">= 0.10"
|
653 |
+
}
|
654 |
+
},
|
655 |
+
"node_modules/qs": {
|
656 |
+
"version": "6.14.0",
|
657 |
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
658 |
+
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
659 |
+
"license": "BSD-3-Clause",
|
660 |
+
"dependencies": {
|
661 |
+
"side-channel": "^1.1.0"
|
662 |
+
},
|
663 |
+
"engines": {
|
664 |
+
"node": ">=0.6"
|
665 |
+
},
|
666 |
+
"funding": {
|
667 |
+
"url": "https://github.com/sponsors/ljharb"
|
668 |
+
}
|
669 |
+
},
|
670 |
+
"node_modules/range-parser": {
|
671 |
+
"version": "1.2.1",
|
672 |
+
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
673 |
+
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
674 |
+
"license": "MIT",
|
675 |
+
"engines": {
|
676 |
+
"node": ">= 0.6"
|
677 |
+
}
|
678 |
+
},
|
679 |
+
"node_modules/raw-body": {
|
680 |
+
"version": "3.0.0",
|
681 |
+
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
|
682 |
+
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
|
683 |
+
"license": "MIT",
|
684 |
+
"dependencies": {
|
685 |
+
"bytes": "3.1.2",
|
686 |
+
"http-errors": "2.0.0",
|
687 |
+
"iconv-lite": "0.6.3",
|
688 |
+
"unpipe": "1.0.0"
|
689 |
+
},
|
690 |
+
"engines": {
|
691 |
+
"node": ">= 0.8"
|
692 |
+
}
|
693 |
+
},
|
694 |
+
"node_modules/router": {
|
695 |
+
"version": "2.2.0",
|
696 |
+
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
697 |
+
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
698 |
+
"license": "MIT",
|
699 |
+
"dependencies": {
|
700 |
+
"debug": "^4.4.0",
|
701 |
+
"depd": "^2.0.0",
|
702 |
+
"is-promise": "^4.0.0",
|
703 |
+
"parseurl": "^1.3.3",
|
704 |
+
"path-to-regexp": "^8.0.0"
|
705 |
+
},
|
706 |
+
"engines": {
|
707 |
+
"node": ">= 18"
|
708 |
+
}
|
709 |
+
},
|
710 |
+
"node_modules/safe-buffer": {
|
711 |
+
"version": "5.2.1",
|
712 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
713 |
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
714 |
+
"funding": [
|
715 |
+
{
|
716 |
+
"type": "github",
|
717 |
+
"url": "https://github.com/sponsors/feross"
|
718 |
+
},
|
719 |
+
{
|
720 |
+
"type": "patreon",
|
721 |
+
"url": "https://www.patreon.com/feross"
|
722 |
+
},
|
723 |
+
{
|
724 |
+
"type": "consulting",
|
725 |
+
"url": "https://feross.org/support"
|
726 |
+
}
|
727 |
+
],
|
728 |
+
"license": "MIT"
|
729 |
+
},
|
730 |
+
"node_modules/safer-buffer": {
|
731 |
+
"version": "2.1.2",
|
732 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
733 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
734 |
+
"license": "MIT"
|
735 |
+
},
|
736 |
+
"node_modules/send": {
|
737 |
+
"version": "1.2.0",
|
738 |
+
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
739 |
+
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
740 |
+
"license": "MIT",
|
741 |
+
"dependencies": {
|
742 |
+
"debug": "^4.3.5",
|
743 |
+
"encodeurl": "^2.0.0",
|
744 |
+
"escape-html": "^1.0.3",
|
745 |
+
"etag": "^1.8.1",
|
746 |
+
"fresh": "^2.0.0",
|
747 |
+
"http-errors": "^2.0.0",
|
748 |
+
"mime-types": "^3.0.1",
|
749 |
+
"ms": "^2.1.3",
|
750 |
+
"on-finished": "^2.4.1",
|
751 |
+
"range-parser": "^1.2.1",
|
752 |
+
"statuses": "^2.0.1"
|
753 |
+
},
|
754 |
+
"engines": {
|
755 |
+
"node": ">= 18"
|
756 |
+
}
|
757 |
+
},
|
758 |
+
"node_modules/serve-static": {
|
759 |
+
"version": "2.2.0",
|
760 |
+
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
761 |
+
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
762 |
+
"license": "MIT",
|
763 |
+
"dependencies": {
|
764 |
+
"encodeurl": "^2.0.0",
|
765 |
+
"escape-html": "^1.0.3",
|
766 |
+
"parseurl": "^1.3.3",
|
767 |
+
"send": "^1.2.0"
|
768 |
+
},
|
769 |
+
"engines": {
|
770 |
+
"node": ">= 18"
|
771 |
+
}
|
772 |
+
},
|
773 |
+
"node_modules/setprototypeof": {
|
774 |
+
"version": "1.2.0",
|
775 |
+
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
776 |
+
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
777 |
+
"license": "ISC"
|
778 |
+
},
|
779 |
+
"node_modules/side-channel": {
|
780 |
+
"version": "1.1.0",
|
781 |
+
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
782 |
+
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
783 |
+
"license": "MIT",
|
784 |
+
"dependencies": {
|
785 |
+
"es-errors": "^1.3.0",
|
786 |
+
"object-inspect": "^1.13.3",
|
787 |
+
"side-channel-list": "^1.0.0",
|
788 |
+
"side-channel-map": "^1.0.1",
|
789 |
+
"side-channel-weakmap": "^1.0.2"
|
790 |
+
},
|
791 |
+
"engines": {
|
792 |
+
"node": ">= 0.4"
|
793 |
+
},
|
794 |
+
"funding": {
|
795 |
+
"url": "https://github.com/sponsors/ljharb"
|
796 |
+
}
|
797 |
+
},
|
798 |
+
"node_modules/side-channel-list": {
|
799 |
+
"version": "1.0.0",
|
800 |
+
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
801 |
+
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
802 |
+
"license": "MIT",
|
803 |
+
"dependencies": {
|
804 |
+
"es-errors": "^1.3.0",
|
805 |
+
"object-inspect": "^1.13.3"
|
806 |
+
},
|
807 |
+
"engines": {
|
808 |
+
"node": ">= 0.4"
|
809 |
+
},
|
810 |
+
"funding": {
|
811 |
+
"url": "https://github.com/sponsors/ljharb"
|
812 |
+
}
|
813 |
+
},
|
814 |
+
"node_modules/side-channel-map": {
|
815 |
+
"version": "1.0.1",
|
816 |
+
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
817 |
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
818 |
+
"license": "MIT",
|
819 |
+
"dependencies": {
|
820 |
+
"call-bound": "^1.0.2",
|
821 |
+
"es-errors": "^1.3.0",
|
822 |
+
"get-intrinsic": "^1.2.5",
|
823 |
+
"object-inspect": "^1.13.3"
|
824 |
+
},
|
825 |
+
"engines": {
|
826 |
+
"node": ">= 0.4"
|
827 |
+
},
|
828 |
+
"funding": {
|
829 |
+
"url": "https://github.com/sponsors/ljharb"
|
830 |
+
}
|
831 |
+
},
|
832 |
+
"node_modules/side-channel-weakmap": {
|
833 |
+
"version": "1.0.2",
|
834 |
+
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
835 |
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
836 |
+
"license": "MIT",
|
837 |
+
"dependencies": {
|
838 |
+
"call-bound": "^1.0.2",
|
839 |
+
"es-errors": "^1.3.0",
|
840 |
+
"get-intrinsic": "^1.2.5",
|
841 |
+
"object-inspect": "^1.13.3",
|
842 |
+
"side-channel-map": "^1.0.1"
|
843 |
+
},
|
844 |
+
"engines": {
|
845 |
+
"node": ">= 0.4"
|
846 |
+
},
|
847 |
+
"funding": {
|
848 |
+
"url": "https://github.com/sponsors/ljharb"
|
849 |
+
}
|
850 |
+
},
|
851 |
+
"node_modules/statuses": {
|
852 |
+
"version": "2.0.2",
|
853 |
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
854 |
+
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
855 |
+
"license": "MIT",
|
856 |
+
"engines": {
|
857 |
+
"node": ">= 0.8"
|
858 |
+
}
|
859 |
+
},
|
860 |
+
"node_modules/toidentifier": {
|
861 |
+
"version": "1.0.1",
|
862 |
+
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
863 |
+
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
864 |
+
"license": "MIT",
|
865 |
+
"engines": {
|
866 |
+
"node": ">=0.6"
|
867 |
+
}
|
868 |
+
},
|
869 |
+
"node_modules/type-is": {
|
870 |
+
"version": "2.0.1",
|
871 |
+
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
872 |
+
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
873 |
+
"license": "MIT",
|
874 |
+
"dependencies": {
|
875 |
+
"content-type": "^1.0.5",
|
876 |
+
"media-typer": "^1.1.0",
|
877 |
+
"mime-types": "^3.0.0"
|
878 |
+
},
|
879 |
+
"engines": {
|
880 |
+
"node": ">= 0.6"
|
881 |
+
}
|
882 |
+
},
|
883 |
+
"node_modules/unpipe": {
|
884 |
+
"version": "1.0.0",
|
885 |
+
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
886 |
+
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
887 |
+
"license": "MIT",
|
888 |
+
"engines": {
|
889 |
+
"node": ">= 0.8"
|
890 |
+
}
|
891 |
+
},
|
892 |
+
"node_modules/vary": {
|
893 |
+
"version": "1.1.2",
|
894 |
+
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
895 |
+
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
896 |
+
"license": "MIT",
|
897 |
+
"engines": {
|
898 |
+
"node": ">= 0.8"
|
899 |
+
}
|
900 |
+
},
|
901 |
+
"node_modules/web-streams-polyfill": {
|
902 |
+
"version": "3.3.3",
|
903 |
+
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
904 |
+
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
905 |
+
"license": "MIT",
|
906 |
+
"engines": {
|
907 |
+
"node": ">= 8"
|
908 |
+
}
|
909 |
+
},
|
910 |
+
"node_modules/wrappy": {
|
911 |
+
"version": "1.0.2",
|
912 |
+
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
913 |
+
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
914 |
+
"license": "ISC"
|
915 |
+
}
|
916 |
+
}
|
917 |
+
}
|
server/package.json
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "server",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"main": "index.js",
|
5 |
+
"scripts": {
|
6 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
7 |
+
},
|
8 |
+
"keywords": [],
|
9 |
+
"author": "",
|
10 |
+
"license": "ISC",
|
11 |
+
"description": "",
|
12 |
+
"dependencies": {
|
13 |
+
"express": "^4.19.2",
|
14 |
+
"fluent-ffmpeg": "^2.1.2",
|
15 |
+
"form-data": "^4.0.0",
|
16 |
+
"multer": "^1.4.5-lts.1",
|
17 |
+
"node-fetch": "^2.7.0"
|
18 |
+
}
|
19 |
+
}
|
server/server.js
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const express = require('express');
|
2 |
+
const multer = require('multer');
|
3 |
+
const FormData = require('form-data');
|
4 |
+
const axios = require('axios'); // 使用 axios 替换 node-fetch
|
5 |
+
const fetch = require('node-fetch'); // 保留用于翻译接口
|
6 |
+
|
7 |
+
const app = express();
|
8 |
+
const port = 3000;
|
9 |
+
|
10 |
+
const storage = multer.memoryStorage();
|
11 |
+
const upload = multer({ storage: storage });
|
12 |
+
|
13 |
+
app.use(express.json());
|
14 |
+
app.use(express.urlencoded({ extended: true }));
|
15 |
+
app.use((req, res, next) => {
|
16 |
+
res.header('Access-Control-Allow-Origin', '*');
|
17 |
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
18 |
+
next();
|
19 |
+
});
|
20 |
+
|
21 |
+
const PYTHON_ASR_SERVICE_URL = 'http://localhost:5001/transcribe';
|
22 |
+
|
23 |
+
// ASR Endpoint: 使用 axios 重写,确保超时正确生效
|
24 |
+
app.post('/asr', upload.single('audio'), async (req, res) => {
|
25 |
+
console.log("Received ASR request, proxying to Python service with axios...");
|
26 |
+
|
27 |
+
if (!req.file) {
|
28 |
+
return res.status(400).json({ error: 'No audio file uploaded.' });
|
29 |
+
}
|
30 |
+
|
31 |
+
try {
|
32 |
+
const form = new FormData();
|
33 |
+
form.append('audio', req.file.buffer, {
|
34 |
+
filename: req.file.originalname,
|
35 |
+
contentType: req.file.mimetype,
|
36 |
+
});
|
37 |
+
|
38 |
+
const langCode = req.body.sourceLang.split('-')[0];
|
39 |
+
form.append('language', langCode);
|
40 |
+
console.log(`Forwarding audio buffer with language '${langCode}'...`);
|
41 |
+
|
42 |
+
// 使用 axios 发送请求,并设置一个可靠的60秒超时
|
43 |
+
const asrResponse = await axios.post(PYTHON_ASR_SERVICE_URL, form, {
|
44 |
+
headers: form.getHeaders(),
|
45 |
+
timeout: 60000 // 60秒超时,作用于整个请求
|
46 |
+
});
|
47 |
+
|
48 |
+
const asrData = asrResponse.data;
|
49 |
+
|
50 |
+
console.log(`Transcription result: "${asrData.transcript}"`);
|
51 |
+
res.json({ transcript: asrData.transcript });
|
52 |
+
|
53 |
+
} catch (error) {
|
54 |
+
if (error.code === 'ECONNABORTED') {
|
55 |
+
console.error("Failed to process ASR request: Axios request timed out.");
|
56 |
+
res.status(504).json({ error: 'Request to ASR service timed out.' });
|
57 |
+
} else {
|
58 |
+
console.error("Failed to process ASR request:", error.message);
|
59 |
+
res.status(500).json({ error: error.message });
|
60 |
+
}
|
61 |
+
}
|
62 |
+
});
|
63 |
+
|
64 |
+
// Translate Endpoint (保持不变)
|
65 |
+
app.post('/translate', async (req, res) => {
|
66 |
+
const { text, sourceLang, targetLang } = req.body;
|
67 |
+
if (!text || !sourceLang || !targetLang) {
|
68 |
+
return res.status(400).json({ error: 'Missing text, sourceLang, or targetLang' });
|
69 |
+
}
|
70 |
+
const source = sourceLang.split('-')[0];
|
71 |
+
const target = targetLang.split('-')[0];
|
72 |
+
if (source === target) {
|
73 |
+
return res.json({ translatedText: text });
|
74 |
+
}
|
75 |
+
const apiUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
|
76 |
+
try {
|
77 |
+
const response = await fetch(apiUrl);
|
78 |
+
const data = await response.json();
|
79 |
+
if (data && data[0] && data[0][0] && data[0][0][0]) {
|
80 |
+
const translatedText = data[0].map(segment => segment[0]).join('');
|
81 |
+
res.json({ translatedText });
|
82 |
+
} else {
|
83 |
+
throw new Error('Unexpected API response from translation service');
|
84 |
+
}
|
85 |
+
} catch (error) {
|
86 |
+
console.error('Translation request error:', error.message);
|
87 |
+
res.status(500).json({ error: 'Translation failed' });
|
88 |
+
}
|
89 |
+
});
|
90 |
+
|
91 |
+
app.get('/', (req, res) => {
|
92 |
+
res.status(200).send('Node.js server is running and healthy!');
|
93 |
+
});
|
94 |
+
|
95 |
+
app.listen(port, () => {
|
96 |
+
console.log(`Node.js proxy server listening at http://localhost:${port}`);
|
97 |
+
});
|
server/server.js.old
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// wechat-miniprogram-translator/server/server.js
|
2 |
+
const express = require('express');
|
3 |
+
const fetch = require('node-fetch');
|
4 |
+
const app = express();
|
5 |
+
const port = 3000;
|
6 |
+
|
7 |
+
app.use(express.json());
|
8 |
+
app.use(express.urlencoded({ extended: true }));
|
9 |
+
|
10 |
+
// CORS for development (allow requests from any origin)
|
11 |
+
app.use((req, res, next) => {
|
12 |
+
res.header('Access-Control-Allow-Origin', '*');
|
13 |
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
14 |
+
next();
|
15 |
+
});
|
16 |
+
|
17 |
+
// ASR (Speech-to-Text) Endpoint - Placeholder
|
18 |
+
app.post('/asr', (req, res) => {
|
19 |
+
// In a real application, you would receive the audio file here (e.g., via multipart/form-data)
|
20 |
+
// and send it to a real ASR service (e.g., Tencent Cloud, Baidu AI, Google Speech-to-Text).
|
21 |
+
// For this prototype, we'll simulate a response.
|
22 |
+
console.log('Received ASR request. Simulating response...');
|
23 |
+
const simulatedText = '这是模拟的语音识别结果。'; // You can change this for testing
|
24 |
+
res.json({ transcript: simulatedText });
|
25 |
+
});
|
26 |
+
|
27 |
+
// Translation Endpoint - Proxy for Google Translate API
|
28 |
+
app.post('/translate', async (req, res) => {
|
29 |
+
const { text, sourceLang, targetLang } = req.body;
|
30 |
+
|
31 |
+
if (!text || !sourceLang || !targetLang) {
|
32 |
+
return res.status(400).json({ error: 'Missing text, sourceLang, or targetLang' });
|
33 |
+
}
|
34 |
+
|
35 |
+
const source = sourceLang.split('-')[0];
|
36 |
+
const target = targetLang.split('-')[0];
|
37 |
+
|
38 |
+
if (source === target) {
|
39 |
+
return res.json({ translatedText: text });
|
40 |
+
}
|
41 |
+
|
42 |
+
const apiUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
|
43 |
+
|
44 |
+
try {
|
45 |
+
const response = await fetch(apiUrl);
|
46 |
+
const data = await response.json();
|
47 |
+
|
48 |
+
if (data && data[0] && data[0][0] && data[0][0][0]) {
|
49 |
+
const translatedText = data[0].map(segment => segment[0]).join('');
|
50 |
+
res.json({ translatedText });
|
51 |
+
} else {
|
52 |
+
console.error('Translation API returned unexpected data:', data);
|
53 |
+
res.status(500).json({ error: 'Translation failed: Unexpected API response' });
|
54 |
+
}
|
55 |
+
} catch (error) {
|
56 |
+
console.error('Error calling Google Translate API:', error);
|
57 |
+
res.status(500).json({ error: 'Translation failed: Network error or API issue' });
|
58 |
+
}
|
59 |
+
});
|
60 |
+
|
61 |
+
app.listen(port, () => {
|
62 |
+
console.log(`Backend server listening at http://localhost:${port}`);
|
63 |
+
console.log("Remember to start this server before running the Mini Program!");
|
64 |
+
});
|
start_asr.sh
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
# This script starts the Gunicorn server for the ASR application.
|
3 |
+
|
4 |
+
echo "Starting ASR service with Gunicorn..."
|
5 |
+
|
6 |
+
# Absolute path to the project directory
|
7 |
+
PROJECT_DIR="/home/ubuntu/wechat-miniprogram-translator"
|
8 |
+
|
9 |
+
# Absolute path to the Gunicorn executable within the venv
|
10 |
+
VENV_GUNICORN="${PROJECT_DIR}/venv/bin/gunicorn"
|
11 |
+
|
12 |
+
# Check if the Gunicorn executable exists
|
13 |
+
if [ ! -f "$VENV_GUNICORN" ]; then
|
14 |
+
echo "Error: Gunicorn executable not found at $VENV_GUNICORN"
|
15 |
+
echo "Please make sure you have run 'pip install gunicorn' in your venv."
|
16 |
+
exit 1
|
17 |
+
fi
|
18 |
+
|
19 |
+
# Navigate to the directory containing asr_server.py
|
20 |
+
cd "$PROJECT_DIR" || exit
|
21 |
+
|
22 |
+
# Explanation of parameters:
|
23 |
+
# --workers 1: Use a single worker process.
|
24 |
+
# --max-requests 1: CRITICAL! Restart the worker after every single request to prevent state issues.
|
25 |
+
# --timeout 120: Allow each worker up to 120 seconds to process a request.
|
26 |
+
# --bind 0.0.0.0:5001: Listen on port 5001 on all network interfaces.
|
27 |
+
# asr_server:app: The Flask application instance 'app' from the 'asr_server.py' file.
|
28 |
+
# "$@": This is the crucial part. It takes all arguments passed to this script
|
29 |
+
# (from ecosystem.config.js, e.g., --model tiny) and passes them to the python script.
|
30 |
+
exec "$VENV_GUNICORN" --workers 1 --max-requests 1 --timeout 120 --bind 0.0.0.0:5001 asr_server:app -- "$@"
|
translate.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
from transformers import MarianMTModel, MarianTokenizer
|
3 |
+
|
4 |
+
def translate_text(text, source_lang, target_lang):
|
5 |
+
"""
|
6 |
+
Translates text from a source language to a target language.
|
7 |
+
"""
|
8 |
+
try:
|
9 |
+
# Define the model name based on source and target languages
|
10 |
+
# The format is 'Helsinki-NLP/opus-mt-{source}-{target}'
|
11 |
+
model_name = f'Helsinki-NLP/opus-mt-{source_lang}-{target_lang}'
|
12 |
+
|
13 |
+
# Load the tokenizer and model.
|
14 |
+
# The first time a model is used, it will be downloaded from Hugging Face.
|
15 |
+
# This might take a moment. Subsequent uses will load from cache.
|
16 |
+
tokenizer = MarianTokenizer.from_pretrained(model_name)
|
17 |
+
model = MarianMTModel.from_pretrained(model_name)
|
18 |
+
|
19 |
+
# Tokenize the input text
|
20 |
+
tokenized_text = tokenizer(text, return_tensors="pt", padding=True)
|
21 |
+
|
22 |
+
# Generate the translation
|
23 |
+
translated_tokens = model.generate(**tokenized_text)
|
24 |
+
|
25 |
+
# Decode the translated tokens into text
|
26 |
+
translated_text = tokenizer.decode(translated_tokens[0], skip_special_tokens=True)
|
27 |
+
|
28 |
+
return translated_text
|
29 |
+
except Exception as e:
|
30 |
+
# Handle cases where a direct model doesn't exist (e.g., zh-es)
|
31 |
+
# or other errors.
|
32 |
+
return f"Error during translation: {str(e)}"
|
33 |
+
|
34 |
+
|
35 |
+
if __name__ == "__main__":
|
36 |
+
# The script expects three arguments: text, source_lang, target_lang
|
37 |
+
if len(sys.argv) != 4:
|
38 |
+
print("Usage: python translate.py <text_to_translate> <source_lang> <target_lang>")
|
39 |
+
sys.exit(1)
|
40 |
+
|
41 |
+
input_text = sys.argv[1]
|
42 |
+
source_language = sys.argv[2]
|
43 |
+
target_language = sys.argv[3]
|
44 |
+
|
45 |
+
# The models use 2-letter language codes (e.g., 'en', 'zh', 'es')
|
46 |
+
# We take the first part of the lang code (e.g., 'zh-CN' -> 'zh')
|
47 |
+
source_code = source_language.split('-')[0]
|
48 |
+
target_code = target_language.split('-')[0]
|
49 |
+
|
50 |
+
translated_output = translate_text(input_text, source_code, target_code)
|
51 |
+
print(translated_output)
|