diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..07d649659c2826650e5d0bcb127b76f3432a2ff7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignore local environment variables and secrets +.env diff --git a/app.js b/app.js new file mode 100644 index 0000000000000000000000000000000000000000..79262787d0a84ec282f66363621bdd77f9e9ade5 --- /dev/null +++ b/app.js @@ -0,0 +1,6 @@ +// app.js +App({ + onLaunch() { + // 小程序启动时执行的逻辑 + } +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000000000000000000000000000000000000..562f4963f8b55669ed15b3dcb130ca5b8361ae94 --- /dev/null +++ b/app.json @@ -0,0 +1,25 @@ +{ + "pages": [ + "pages/index/index" + ], + "permission": { + "scope.record": { + "desc": "用于实时语音识别" + } + }, + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#fff", + "navigationBarTitleText": "实时翻译", + "navigationBarTextStyle": "black" + }, + "networkTimeout": { + "request": 10000, + "connectSocket": 10000, + "uploadFile": 10000, + "downloadFile": 10000 + }, + "requiredBackgroundModes": ["audio"], + "style": "v2", + "sitemapLocation": "sitemap.json" +} \ No newline at end of file diff --git a/app.py b/app.py index 07e0e15b820254a03d535db7d6ecc29e89999ee3..3622a9c2048680bf2fdbdccfbdda419614fa73b3 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,4 @@ -import gradio as gr -from transformers import pipeline, AutoProcessor +from transformers import AutoProcessor from optimum.onnxruntime import ORTModelForSpeechSeq2Seq import torch import os @@ -11,6 +10,10 @@ import uvicorn import deepl from dotenv import load_dotenv import soundfile as sf +import logging + +# --- Basic Configuration --- +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # --- Load environment variables and initialize DeepL --- load_dotenv() @@ -20,157 +23,188 @@ deepl_translator = None if DEEPL_AUTH_KEY: try: deepl_translator = deepl.Translator(DEEPL_AUTH_KEY) - print("DeepL translator initialized successfully.") + logging.info("DeepL translator initialized successfully.") except Exception as e: - print(f"Error initializing DeepL translator: {e}") - print("DeepL will be unavailable.") + logging.error(f"Error initializing DeepL translator: {e}") else: - print("DEEPL_AUTH_KEY not found. DeepL will be unavailable.") -# --- End --- - + logging.warning("DEEPL_AUTH_KEY not found. DeepL will be unavailable.") -# 1. Load Models -print("Loading all models... This will take some time on startup.") +# --- Load Models --- +logging.info("Loading all models...") -# ASR Model - Using a CPU-optimized ONNX model for speed -print("Loading optimized ASR model...") +# ASR Model asr_model_id = "openai/whisper-base" - -# Load the model and processor using Optimum for ONNX runtime acceleration -asr_model = ORTModelForSpeechSeq2Seq.from_pretrained(asr_model_id, provider="CPUExecutionProvider") -asr_processor = AutoProcessor.from_pretrained(asr_model_id) -print("Optimized ASR model loaded.") - - -# Translation Pipelines - Reverting to the 6 core, absolutely reliable models -translators = { - "en-zh": pipeline("translation", model="Helsinki-NLP/opus-mt-en-zh"), - "zh-en": pipeline("translation", model="Varine/opus-mt-zh-en-model"), - "en-ja": pipeline("translation", model="staka/fugumt-en-ja"), - "ja-en": pipeline("translation", model="Helsinki-NLP/opus-mt-ja-en"), - "en-ko": pipeline("translation", model="Helsinki-NLP/opus-mt-tc-big-en-ko"), - "ko-en": pipeline("translation", model="Helsinki-NLP/opus-mt-ko-en"), -} - -print("All models loaded successfully.") - -# 2. Define Core Logic Functions +asr_model = None +asr_processor = None +try: + asr_model = ORTModelForSpeechSeq2Seq.from_pretrained(asr_model_id, provider="CPUExecutionProvider") + asr_processor = AutoProcessor.from_pretrained(asr_model_id) + + # FINAL, CRITICAL FIX: The model's default config has a conflicting 'forced_decoder_ids' + # that clashes with the latest library versions. The library both requires this attribute + # to exist, but also requires it to be None to avoid a conflict. + if hasattr(asr_model.config, 'forced_decoder_ids'): + logging.info("Found conflicting 'forced_decoder_ids' in model config. Setting to None.") + asr_model.config.forced_decoder_ids = None + + if hasattr(asr_model.generation_config, 'forced_decoder_ids'): + logging.info("Found conflicting 'forced_decoder_ids' in generation_config. Setting to None.") + asr_model.generation_config.forced_decoder_ids = None + + logging.info("ASR model and processor loaded and configured successfully.") +except Exception as e: + logging.error(f"Fatal error loading ASR model: {e}", exc_info=True) + +# Translation Pipelines +from transformers import pipeline +translators = {} +try: + translators = { + "en-zh": pipeline("translation", model="Helsinki-NLP/opus-mt-en-zh"), + "zh-en": pipeline("translation", model="Helsinki-NLP/opus-mt-zh-en"), + "en-ja": pipeline("translation", model="staka/fugumt-en-ja"), + "ja-en": pipeline("translation", model="Helsinki-NLP/opus-mt-ja-en"), + "en-ko": pipeline("translation", model="Helsinki-NLP/opus-mt-tc-big-en-ko"), + "ko-en": pipeline("translation", model="Helsinki-NLP/opus-mt-ko-en"), + } + logging.info("Translation models loaded successfully.") +except Exception as e: + logging.error(f"Failed to load translation models: {e}") + +# --- Core Logic Functions --- def transcribe_audio(audio_bytes): + if not asr_model or not asr_processor: + return None, "ASR model or processor is not available." try: - # Use a temporary file to handle the audio bytes with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file: tmp_file.write(audio_bytes) audio_path = tmp_file.name - # Read the audio file and process it audio_input, sample_rate = sf.read(audio_path) - - # Ensure the audio is in the correct format (mono, 16kHz) if audio_input.ndim > 1: - audio_input = audio_input.mean(axis=1) # to mono - if sample_rate != 16000: - # This is a placeholder for resampling. For now, we assume frontend sends 16kHz. - pass + audio_input = audio_input.mean(axis=1) - # Process audio and generate token IDs input_features = asr_processor(audio_input, sampling_rate=16000, return_tensors="pt").input_features - predicted_ids = asr_model.generate(input_features) - # Decode the token IDs to text + # By setting forced_decoder_ids to None in the config, we can now safely + # let the generate function handle the task without conflicts. + predicted_ids = asr_model.generate(input_features, task="transcribe") + text = asr_processor.batch_decode(predicted_ids, skip_special_tokens=True)[0] + + logging.info(f"ASR transcribed text: '{text}'") os.remove(audio_path) return text, None except Exception as e: - # Clean up the temp file in case of an error + logging.error(f"ASR transcription failed: {e}", exc_info=True) if 'audio_path' in locals() and os.path.exists(audio_path): os.remove(audio_path) return None, str(e) def translate_text(text, source_lang, target_lang): - if not text or not source_lang or not target_lang: - return "", None - if source_lang == target_lang: - return text, None - - # --- DeepL Hybrid Logic --- - if source_lang == 'zh' and target_lang == 'ja' and deepl_translator: - print("Attempting translation with DeepL for zh -> ja") + # Priority 1: Use DeepL for specific, high-quality pairs if available + if deepl_translator and ((source_lang == 'zh' and target_lang == 'ja') or (source_lang == 'en' and target_lang == 'ja')): try: - result = deepl_translator.translate_text(text, target_lang="JA") + dl_source_lang = "ZH" if source_lang == 'zh' else "EN" + logging.info(f"Attempting DeepL translation for {source_lang} -> {target_lang}") + result = deepl_translator.translate_text(text, source_lang=dl_source_lang, target_lang="JA") return result.text, None except Exception as e: - print(f"DeepL API call failed: {e}. Falling back to Hugging Face model.") - # --- End --- - - key = f"{source_lang}-{target_lang}" - try: - if key in translators: - return translators[key](text)[0]['translation_text'], None - - elif source_lang != 'en' and target_lang != 'en': - if f"{source_lang}-en" not in translators or f"en-{target_lang}" not in translators: - return None, f"Bridge translation route not supported: {source_lang}-en or en-{target_lang}" - - print(f"Performing bridge translation: {source_lang} -> en -> {target_lang}") - english_text = translators[f"{source_lang}-en"](text)[0]['translation_text'] - return translators[f"en-{target_lang}"](english_text)[0]['translation_text'], None - else: - return None, f"Translation route not supported: {key}" - except Exception as e: - return None, str(e) + logging.error(f"DeepL failed: {e}. Falling back to HF models.") -# 3. Create FastAPI App + # Priority 2: Try direct HF translation + model_key = f"{source_lang}-{target_lang}" + translator = translators.get(model_key) + if translator: + try: + logging.info(f"Attempting direct HF translation for {model_key}") + translated_text = translator(text, max_length=512)[0]['translation_text'] + return translated_text, None + except Exception as e: + logging.error(f"Direct HF translation for {model_key} failed: {e}", exc_info=True) + # Don't return here, allow fallback to pivot + + # Priority 3: Try pivot translation via English + if source_lang != 'en' and target_lang != 'en': + to_en_key = f"{source_lang}-en" + from_en_key = f"en-{target_lang}" + translator_to_en = translators.get(to_en_key) + translator_from_en = translators.get(from_en_key) + + if translator_to_en and translator_from_en: + try: + logging.info(f"Attempting pivot translation for {source_lang} -> en -> {target_lang}") + # Step 1: Source to English + english_text = translator_to_en(text, max_length=512)[0]['translation_text'] + logging.info(f"Pivot step (to en) result: '{english_text}'") + # Step 2: English to Target + final_text = translator_from_en(english_text, max_length=512)[0]['translation_text'] + logging.info(f"Pivot step (from en) result: '{final_text}'") + return final_text, None + except Exception as e: + logging.error(f"Pivot translation failed: {e}", exc_info=True) + + # If all else fails + logging.warning(f"No translation path found for {source_lang} -> {target_lang}") + return None, f"No model available for {source_lang} to {target_lang}" + +# --- FastAPI App --- app = FastAPI() -# 4. Define API Endpoints with FastAPI +@app.get("/") +def root(): + return {"status": "ok", "message": "Translator API is running."} + @app.post("/api/asr") async def api_asr(request: Request): - json_data = await request.json() - audio_data_uri = json_data.get('audio_data_uri') - if not audio_data_uri: - return JSONResponse(status_code=400, content={"error": "No audio_data_uri provided"}) try: - header, encoded = audio_data_uri.split(",", 1) - audio_bytes = base64.b64decode(encoded) - transcript, error = transcribe_audio(audio_bytes) + body = await request.json() + audio_b64 = body.get('audio_base64') + if not audio_b64: + logging.error("Request is missing 'audio_base64'") + return JSONResponse(status_code=400, content={"error": "No audio_base64 found in request"}) + + audio_bytes = base64.b64decode(audio_b64) + + text, error = transcribe_audio(audio_bytes) + if error: + logging.error(f"ASR transcription function returned an error: {error}") return JSONResponse(status_code=500, content={"error": f"ASR Error: {error}"}) - return JSONResponse(status_code=200, content={"transcript": transcript}) + + response_data = {"text": text} + logging.info(f"Returning ASR response: {response_data}") + return JSONResponse(content=response_data) + except Exception as e: - return JSONResponse(status_code=500, content={"error": f"Server error: {e}"}) + logging.error(f"Critical error in /api/asr endpoint: {e}", exc_info=True) + return JSONResponse(status_code=500, content={"error": str(e)}) @app.post("/api/translate") async def api_translate(request: Request): - json_data = await request.json() - text = json_data.get('text') - source_lang = json_data.get('source_lang') - target_lang = json_data.get('target_lang') - if not all([text, source_lang, target_lang]): - return JSONResponse(status_code=400, content={"error": "Missing parameters"}) - - translated_text, error = translate_text(text, source_lang, target_lang) - if error: - return JSONResponse(status_code=500, content={"error": error}) - return JSONResponse(status_code=200, content={"translated_text": translated_text}) - -# 5. Create a simple Gradio UI for debugging (Optional) -def gradio_asr(audio_file): - if audio_file is None: - return "" - # Gradio provides a file object, read its bytes - audio_input, sample_rate = sf.read(audio_file.name) - # Process audio and generate token IDs - input_features = asr_processor(audio_input, sampling_rate=sample_rate, return_tensors="pt").input_features - predicted_ids = asr_model.generate(input_features) - # Decode the token IDs to text - transcript = asr_processor.batch_decode(predicted_ids, skip_special_tokens=True)[0] - return transcript - -gradio_ui = gr.Interface(fn=gradio_asr, inputs=gr.Audio(type="filepath"), outputs="text", title="ASR Debugger") - -# 6. Mount Gradio app onto FastAPI -app = gr.mount_gradio_app(app, gradio_ui, path="/") + try: + body = await request.json() + text = body.get('text') + source_lang = body.get('source_lang') + target_lang = body.get('target_lang') + + if not all([text, source_lang, target_lang]): + return JSONResponse(status_code=400, content={"error": "Missing parameters: text, source_lang, or target_lang"}) + + translated_text, error = translate_text(text, source_lang, target_lang) + + if error: + return JSONResponse(status_code=500, content={"error": f"Translation Error: {error}"}) + + response_data = {"translated_text": translated_text} + logging.info(f"Returning translation response: {response_data}") + return JSONResponse(content=response_data) + + except Exception as e: + logging.error(f"Error in /api/translate endpoint: {e}", exc_info=True) + return JSONResponse(status_code=500, content={"error": str(e)}) +# --- Main Execution --- if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=7860) \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=7860) diff --git a/asr.py b/asr.py new file mode 100644 index 0000000000000000000000000000000000000000..76b658b3bbb51a92bc4ff07ca37e2319f3f925de --- /dev/null +++ b/asr.py @@ -0,0 +1,31 @@ +import sys +import whisper + +def transcribe_audio(file_path): + """ + Transcribes an audio file using Whisper. + """ + try: + # Load the base model. You can change this to 'tiny', 'small', 'medium', or 'large' + # depending on your server's performance and desired accuracy. + # 'base' is a good starting point. + model = whisper.load_model("base") + + # Transcribe the audio file + result = model.transcribe(file_path) + + # Return the transcribed text + return result["text"] + except Exception as e: + # Return the error message if something goes wrong + return f"Error during transcription: {str(e)}" + +if __name__ == "__main__": + # The script expects exactly one argument: the audio file path + if len(sys.argv) != 2: + print("Usage: python asr.py ") + sys.exit(1) + + audio_file = sys.argv[1] + transcribed_text = transcribe_audio(audio_file) + print(transcribed_text) diff --git a/asr_server.py b/asr_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c29bd272e73def0d09122294ba3f5396a65b59e3 --- /dev/null +++ b/asr_server.py @@ -0,0 +1,78 @@ +import flask +from flask import request, jsonify +import logging +import numpy as np +import ffmpeg +from faster_whisper import WhisperModel +import os + +# --- Configuration from Environment Variables --- +MODEL_NAME = os.getenv("MODEL_NAME", "small") +CPU_THREADS = int(os.getenv("CPU_THREADS", "2")) + +# --- Setup --- +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +# --- Load Model with Performance Optimizations --- +model = None +try: + logging.info(f"Loading faster-whisper model '{MODEL_NAME}' with {CPU_THREADS} threads...") + # 回退到兼容性最好的 int8 计算模式 + model = WhisperModel(MODEL_NAME, device="cpu", compute_type="int8", cpu_threads=CPU_THREADS, num_workers=1) + logging.info(f"Whisper '{MODEL_NAME}' model loaded successfully.") +except Exception as e: + logging.error(f"Fatal error: Could not load model. {e}") + exit(1) + +# --- Initialize Flask App --- +app = flask.Flask(__name__) + +def load_audio_from_buffer(buffer, sr=16000): + """Decodes audio from an in-memory buffer using ffmpeg.""" + try: + out, _ = ( + ffmpeg.input('pipe:', threads=0) + .output('pipe:', format='s16le', acodec='pcm_s16le', ac=1, ar=sr) + .run(input=buffer, capture_stdout=True, capture_stderr=True) + ) + except ffmpeg.Error as e: + raise RuntimeError(f"Failed to load audio with ffmpeg: {e.stderr.decode()}") from e + return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0 + +@app.route('/transcribe', methods=['POST']) +def transcribe_audio(): + if 'audio' not in request.files: + return jsonify({"error": "No audio file part in the request"}), 400 + + file = request.files['audio'] + if file.filename == '': + return jsonify({"error": "No selected file"}), 400 + + try: + audio_buffer = file.read() + audio_np = load_audio_from_buffer(audio_buffer) + + source_lang_full = request.form.get('sourceLang') + lang_code = None + if source_lang_full: + lang_code = source_lang_full.split('-')[0] + logging.info(f"Client requested language: {lang_code}") + + # 保留有���的性能优化: 减小 beam_size 并调整 VAD + segments, info = model.transcribe( + audio_np, + language=lang_code, + beam_size=2, # 从默认的5减小到2,显著降低计算量 + vad_filter=True, + vad_parameters=dict(min_silence_duration_ms=700) # 稍微增加静音检测时长 + ) + + transcript = "".join(segment.text for segment in segments).strip() + + logging.info(f"Detected language '{info.language}' with probability {info.language_probability:.2f}") + logging.info(f"Transcription result: '{transcript}'") + return jsonify({"transcript": transcript}) + + except Exception as e: + logging.error(f"Error during transcription: {e}", exc_info=True) + return jsonify({"error": f"Transcription failed: {str(e)}"}), 500 diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000000000000000000000000000000000000..6ffdde1bdac1ee999471f9d07b5c79d1a220a6cd --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,8 @@ +module.exports = { + apps: [ + { + name: 'translator-server', + script: './server/server.js', + }, + ], +}; \ No newline at end of file diff --git a/pages/index/.DS_Store b/pages/index/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 Binary files /dev/null and b/pages/index/.DS_Store differ diff --git "a/pages/index/index-original-\345\205\250\351\203\250\344\276\235\346\211\230\344\272\216HF\350\277\233\350\241\214\350\257\206\345\210\253\347\277\273\350\257\221\347\232\204\347\211\210\346\234\254-\345\217\257\344\273\245\350\277\220\350\241\214.js" "b/pages/index/index-original-\345\205\250\351\203\250\344\276\235\346\211\230\344\272\216HF\350\277\233\350\241\214\350\257\206\345\210\253\347\277\273\350\257\221\347\232\204\347\211\210\346\234\254-\345\217\257\344\273\245\350\277\220\350\241\214.js" new file mode 100644 index 0000000000000000000000000000000000000000..eae6186cfaccf6e1dd88a0be1a9daa486e45bf11 --- /dev/null +++ "b/pages/index/index-original-\345\205\250\351\203\250\344\276\235\346\211\230\344\272\216HF\350\277\233\350\241\214\350\257\206\345\210\253\347\277\273\350\257\221\347\232\204\347\211\210\346\234\254-\345\217\257\344\273\245\350\277\220\350\241\214.js" @@ -0,0 +1,174 @@ +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn' }, + 'en': { name: 'English', flag: 'us' }, + 'ja': { name: '日本語', flag: 'jp' }, + 'ko': { name: '한국어', flag: 'kr' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.recorderManager = wx.getRecorderManager(); + this.initRecorderManager(); + }, + + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + const sourceLanguages = langCodes.map(code => ({ + langCode: code, + name: languages[code].name, + flag: languages[code].flag, + selected: code === sourceLang + })); + const targetLanguages = langCodes.map(code => ({ + langCode: code, + name: languages[code].name, + flag: languages[code].flag, + selected: code === targetLang + })); + this.setData({ sourceLanguages, targetLanguages }); + }, + + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, () => { + this.initializeLanguages(); + if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') { + this.translate(this.data.transcript); + } + }); + }, + + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, () => { + this.initializeLanguages(); + if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') { + this.translate(this.data.transcript); + } + }); + }, + + swapLanguages: function () { + let { sourceLang, targetLang, transcript, outputText } = this.data; + const tempLang = sourceLang; + sourceLang = targetLang; + targetLang = tempLang; + const tempText = transcript; + transcript = outputText; + outputText = tempText; + this.setData({ sourceLang, targetLang, transcript, outputText }, () => { + this.initializeLanguages(); + if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') { + this.translate(this.data.transcript); + } + }); + }, + + initRecorderManager: function () { + this.recorderManager.onStart(() => { + this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' }); + }); + + this.recorderManager.onStop((res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件创建失败' }); + } + }); + + this.recorderManager.onError(() => { + this.setData({ isRecording: false, transcript: '语音识别出错' }); + }); + }, + + startRecording: function () { + this.recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' }); + }, + + stopRecording: function () { + this.recorderManager.stop(); + }, + + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ + filePath: filePath, + encoding: 'base64', + success: (res) => { + const base64Data = res.data; + const dataUri = 'data:audio/mp3;base64,' + base64Data; + + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": dataUri }, + timeout: 60000, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.transcript) { + const transcript = res.data.transcript; + this.setData({ transcript: transcript }); + this.translate(transcript); + } else { + this.setData({ transcript: '语音识别失败' }); + console.error('ASR Error:', res.data.error); + } + }, + fail: (err) => { + this.setData({ transcript: '语音识别请求失败' }); + } + }); + }, + fail: (err) => { + this.setData({ transcript: '读取音频文件失败' }); + } + }); + }, + + translate: function (text) { + if (!text) return; + const { sourceLang, targetLang } = this.data; + if (sourceLang === targetLang) { + this.setData({ outputText: text }); + return; + } + this.setData({ outputText: '正在翻译...' }); + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { + "text": text, + "source_lang": sourceLang, + "target_lang": targetLang + }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.translated_text) { + this.setData({ outputText: res.data.translated_text }); + } else { + this.setData({ outputText: '翻译失败' }); + console.error('Translate Error:', res.data.error); + } + }, + fail: (err) => { + this.setData({ outputText: '翻译出错' }); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index.js b/pages/index/index.js index a0294f2c6f5d5fd6abf29ddd0a6d2a75ac9d28e2..8b7a33b9b89c226a2fc69538b7311bf9773c550e 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -1,4 +1,4 @@ -// FINAL VERSION: v29 - Based on the user's working original file +// FINAL VERSION: v30 - Robust error handling and UI updates // Helper function to show detailed errors function showDetailedError(title, content) { @@ -26,28 +26,24 @@ Page({ }, onLoad: function () { - // Use the working pattern: attach recorderManager to `this` this.recorderManager = wx.getRecorderManager(); this.initRecorderManager(); - - // Use the improved, simpler language list setup this.setData({ sourceLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key })), targetLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key })) }); }, - // --- Language Selection & UI (Simplified) --- + // --- Language Selection & UI --- selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }); }, selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }); }, swapLanguages: function () { this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript }); }, - // --- Unified Native Recorder Initialization (Correct Pattern) --- + // --- Recorder Initialization --- initRecorderManager: function () { this.recorderManager.onStart(() => { - // Correct pattern: Set UI state *inside* the onStart callback this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' }); }); @@ -56,7 +52,6 @@ Page({ if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); } else { - // This case might happen if recording is too short this.setData({ transcript: '录音时间太短或无效' }); } }); @@ -67,7 +62,7 @@ Page({ }); }, - // --- Main Record Button Handler (with Permissions) --- + // --- Record Button Handler --- handleRecordToggle: function() { if (this.data.isRecording) { this.stopRecording(); @@ -85,14 +80,14 @@ Page({ }); }, - // --- Unified Start/Stop Recording --- + // --- Start/Stop Recording --- startRecording: function () { const options = { - duration: 60000, // Max recording duration: 60s - sampleRate: 16000, // For ASR, 16kHz is the standard - numberOfChannels: 1, // Mono audio is sufficient - encodeBitRate: 48000, // 48kbps is a good balance for speech - format: 'mp3' // Use mp3 format + duration: 60000, + sampleRate: 16000, + numberOfChannels: 1, + encodeBitRate: 48000, + format: 'mp3' }; this.recorderManager.start(options); }, @@ -101,25 +96,41 @@ Page({ this.recorderManager.stop(); }, - // --- Unified Backend ASR & Translation Flow --- + // --- ASR & Translation Flow --- uploadAudioForASR: function (filePath) { this.setData({ transcript: '正在识别...' }); wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => { wx.request({ url: `${this.data.hfSpaceUrl}/api/asr`, method: 'POST', - data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, - timeout: 60000, + data: { "audio_base64": res.data }, + timeout: 120000, success: (asrRes) => { - if (asrRes.statusCode === 200 && asrRes.data.transcript) { - const transcript = asrRes.data.transcript; - this.setData({ transcript }); - this.translate(transcript); + console.log("ASR Response:", asrRes); // Log for debugging + + if (asrRes.statusCode === 200 && asrRes.data && typeof asrRes.data.text !== 'undefined') { + const transcript = asrRes.data.text; + if (transcript) { + this.setData({ transcript }); + this.translate(transcript); + } else { + // Handle successful response with empty transcript + this.setData({ transcript: '未能识别到语音,请重试。' }); + } } else { - showDetailedError('语音识别失败', asrRes.data); + // Handle non-200 responses or malformed data + this.setData({ transcript: '识别失败,请重试。' }); + showDetailedError('语音识别失败', asrRes.data || '服务器返回异常'); } }, - fail: (err) => showDetailedError('识别请求失败', err) + fail: (err) => { + this.setData({ transcript: '识别请求失败。' }); + if (err.errMsg && err.errMsg.includes('timeout')) { + showDetailedError('识别超时', '服务器处理时间过长,请稍后再试或尝试更短的语音。'); + } else { + showDetailedError('识别请求失败', err); + } + } }); }}); }, @@ -137,13 +148,17 @@ Page({ data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, timeout: 45000, success: (res) => { - if (res.statusCode === 200 && res.data.translated_text) { + if (res.statusCode === 200 && res.data && res.data.translated_text) { this.setData({ outputText: res.data.translated_text }); } else { - showDetailedError('翻译失败', res.data); + this.setData({ outputText: '翻译失败' }); + showDetailedError('翻译失败', res.data || '服务器返回异常'); } }, - fail: (err) => showDetailedError('翻译请求失败', err) + fail: (err) => { + this.setData({ outputText: '翻译请求失败' }); + showDetailedError('翻译请求失败', err); + } }); } }); \ No newline at end of file diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000000000000000000000000000000000000..f35140f3d61549ea71a3723fc1463ed6e5bef132 --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,49 @@ + + + + + + + + 您说的话 (选择语言) + + + + {{item.name}} + + + + + {{transcript}} + + + + + + + + + + + 翻译成 (选择语言) + + + + {{item.name}} + + + + + {{outputText}} + + + + + + + + \ No newline at end of file diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000000000000000000000000000000000000..e92361b1a1b7334fe49e253bf13b88434deae6a5 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,112 @@ +/* wechat-miniprogram-translator/index.wxss */ +:root { + --background-color: #f0f2f5; + --card-bg: #ffffff; + --primary-text: #1c1e21; + --secondary-text: #606770; + --border-color: #e0e0e0; + --accent-color: #007bff; + --mic-button-bg: #28a745; + --mic-recording-bg: #dc3545; + /* Note: WeChat Mini Program does not fully support custom font families directly via CSS import. */ + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; +} +page { + height: 100%; + background-color: var(--background-color); +} +body { + /* In Mini Program, use 'page' for root styling, 'body' is not applicable */ +} +.container { + width: 100%; + max-width: 500px; /* Optimized for mobile width */ + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 1rem; + height: 100vh; /* Use 100vh for full height in Mini Program */ + padding: 1rem; + box-sizing: border-box; +} + +.io-container { + display: flex; + flex-direction: column; + gap: 100rpx; /* Adjust this value as needed */ +} +.language-panel { + background-color: var(--card-bg); + padding: 0.75rem; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} +.panel-label { + font-size: 0.9rem; + font-weight: 600; + margin-bottom: 0.75rem; + text-align: center; + color: var(--primary-text); +} +.language-buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} +.lang-button { + display: inline-flex; /* Use inline-flex to allow spacing */ + flex-direction: column; + align-items: center; + justify-content: center; + width: 44px; /* Set a fixed width for the button */ + height: 44px; /* Set a fixed height to make it a square */ + margin: 5px; + padding: 0; + border: 2px solid transparent; /* Start with a transparent border */ + border-radius: 50%; /* Make it a circle */ + background-color: #f0f0f0; + transition: all 0.2s ease-in-out; + box-sizing: border-box; /* Ensure padding and border are inside the width/height */ +} + +.lang-button.selected { + border-color: #007aff; /* Blue border for selected state */ + transform: scale(1.1); /* Slightly enlarge the selected button */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +} + +.flag-icon { + width: 32px; /* Adjust flag size to fit inside the circle */ + height: 32px; + border-radius: 50%; /* Make the flag image itself circular */ + object-fit: cover; /* Ensure the image covers the area nicely */ +} + +.mic-container { + position: fixed; + bottom: 30px; + left: 50%; + transform: translateX(-50%); + z-index: 100; +} + +#mic-btn { + width: 70px; + height: 70px; + border-radius: 50%; + background-color: #007aff; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 30px; + box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4); + transition: all 0.2s ease; +} + +#mic-btn.recording { + background-color: #ff3b30; /* Red color when recording */ + transform: scale(1.1); + box-shadow: 0 6px 16px rgba(255, 59, 48, 0.5); +} \ No newline at end of file diff --git a/pages/index/index_v10_old.js b/pages/index/index_v10_old.js new file mode 100644 index 0000000000000000000000000000000000000000..8d785d43e6964f22faf7ed6bb7d951829b497a59 --- /dev/null +++ b/pages/index/index_v10_old.js @@ -0,0 +1,160 @@ +const plugin = requirePlugin('WechatSI'); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); // Correctly initialize the manager within the page lifecycle + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- CORRECTED: Unified, Lifecycle-Aware Recording Logic --- + initRecorderManager: function() { + const manager = plugin.getRecordRecognitionManager(); + + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + // CRITICAL: Only use onStop for final results to prevent race conditions. + manager.onStop = (res) => { + this.setData({ isRecording: false }); + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + if (isChineseEnglish) { + // Mode 1: Plugin handles both ASR and Translation + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + } else { + // Mode 2: Plugin handles ASR, then HF handles translation + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + + // CRITICAL: Save the manager instance to the page context + this.manager = manager; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + this.manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code }); + } else { + this.manager.start({ lang: this.data.languages[sourceLang].code, translate: false }); + } + }, + + stopRecording: function () { + this.manager.stop(); + }, + + // --- HF Bridge Translation Flow (This part remains the same) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v11_old.js b/pages/index/index_v11_old.js new file mode 100644 index 0000000000000000000000000000000000000000..8b3b6d6c700035db4f5329962b4e1542f22ffbfe --- /dev/null +++ b/pages/index/index_v11_old.js @@ -0,0 +1,79 @@ +const plugin = requirePlugin('WechatSI'); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' } + }, + langCodes: ['zh', 'en'], // Only CN and EN for this test version + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [] + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); + }, + + // --- Language Selection Logic (Simplified for CN/EN only) --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Pure Plugin Mode) --- + initRecorderManager: function() { + const manager = plugin.getRecordRecognitionManager(); + + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + // Only onStop is used for final results + manager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + + // CRITICAL: Save the manager instance to the page context + this.manager = manager; + }, + + // --- Recording Start/Stop (Pure Plugin Mode) --- + startRecording: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ isRecording: true }); + this.manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code }); + }, + + stopRecording: function () { + this.manager.stop(); + } +}); \ No newline at end of file diff --git a/pages/index/index_v12_old.js b/pages/index/index_v12_old.js new file mode 100644 index 0000000000000000000000000000000000000000..5b7fd31edd16a965a130efc42698a6538b94b805 --- /dev/null +++ b/pages/index/index_v12_old.js @@ -0,0 +1,83 @@ +const plugin = requirePlugin('WechatSI'); + +Page({ + data: { + languages: { + 'zh_CN': { name: '中文', flag: 'cn' }, + 'en_US': { name: 'English', flag: 'us' } + }, + langCodes: ['zh_CN', 'en_US'], // Only CN and EN for this test version + sourceLang: 'zh_CN', + targetLang: 'en_US', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [] + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); + }, + + // --- Language Selection Logic (Simplified for CN/EN only) --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Pure Plugin Mode) --- + initRecorderManager: function() { + const manager = plugin.getRecordRecognitionManager(); + + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + // Only onStop is used for final results + manager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + + // CRITICAL: Save the manager instance to the page context + this.manager = manager; + }, + + // --- Recording Start/Stop (Pure Plugin Mode) --- + startRecording: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ isRecording: true }); + // CORRECTED: Use trans_lang instead of lto + this.manager.start({ + lang: this.data.languages[sourceLang].code, + trans_lang: this.data.languages[targetLang].code, + }); + }, + + stopRecording: function () { + this.manager.stop(); + } +}); \ No newline at end of file diff --git a/pages/index/index_v13_old.js b/pages/index/index_v13_old.js new file mode 100644 index 0000000000000000000000000000000000000000..d4c52e3a7d0171ef013fb49ce4894dfd130c2666 --- /dev/null +++ b/pages/index/index_v13_old.js @@ -0,0 +1,79 @@ +const plugin = requirePlugin('WechatSI'); +const manager = plugin.getRecordRecognitionManager(); // CRITICAL: Global creation + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' } + }, + langCodes: ['zh', 'en'], // Only CN and EN for this test version + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [] + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); // Set up event listeners for the globally created manager + }, + + // --- Language Selection Logic (Simplified for CN/EN only) --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Pure Plugin Mode) --- + initRecorderManager: function() { + // Event listeners are set on the globally created manager + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + // Only onStop is used for final results + manager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + // --- Recording Start/Stop (Pure Plugin Mode) --- + startRecording: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ isRecording: true }); + manager.start({ + lang: this.data.languages[sourceLang].code, + trans_lang: this.data.languages[targetLang].code, + }); + }, + + stopRecording: function () { + manager.stop(); + } +}); \ No newline at end of file diff --git "a/pages/index/index_v14_\345\220\214\345\243\260\344\274\240\350\257\221API_only-\345\217\257\344\273\245\350\277\220\350\241\214_old.js" "b/pages/index/index_v14_\345\220\214\345\243\260\344\274\240\350\257\221API_only-\345\217\257\344\273\245\350\277\220\350\241\214_old.js" new file mode 100644 index 0000000000000000000000000000000000000000..03141e4323fd41a854d3ba1c31ae11b7a6d0142e --- /dev/null +++ "b/pages/index/index_v14_\345\220\214\345\243\260\344\274\240\350\257\221API_only-\345\217\257\344\273\245\350\277\220\350\241\214_old.js" @@ -0,0 +1,223 @@ + +const plugin = requirePlugin("WechatSI"); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh_CN': { name: '中文', flag: 'cn' }, + 'en_US': { name: 'English', flag: 'us' }, + 'ja_JP': { name: '日本語', flag: 'jp' }, + 'ko_KR': { name: '한국어', flag: 'kr' } + }, + langCodes: ['zh_CN', 'en_US', 'ja_JP', 'ko_KR'], + sourceLang: 'zh_CN', + targetLang: 'en_US', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [] + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecord(); + }, + + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + const sourceLanguages = langCodes.map(code => ({ + langCode: code, + name: languages[code].name, + flag: languages[code].flag, + selected: code === sourceLang + })); + const targetLanguages = langCodes.map(code => ({ + langCode: code, + name: languages[code].name, + flag: languages[code].flag, + selected: code === targetLang + })); + this.setData({ sourceLanguages, targetLanguages }); + }, + + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, this.initializeLanguages); + }, + + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, this.initializeLanguages); + }, + + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + initRecord: function () { + manager.onStart = (res) => { + console.log("成功开始录音识别", res); + this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' }); + }; + + manager.onRecognize = (res) => { + this.setData({ transcript: res.result }); + }; + + manager.onStop = (res) => { + console.log("录音识别结束", res); + this.setData({ isRecording: false, transcript: res.result }); + if (res.result) { + this.translate(res.result); + } else { + this.setData({ transcript: '未能识别到语音' }); + } + }; + + manager.onError = (res) => { + console.error("录音识别错误", res); + this.setData({ isRecording: false, transcript: '语音识别出错' }); + }; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + + // 插件仅支持中英文 + if ((sourceLang !== 'zh_CN' && sourceLang !== 'en_US') || (targetLang !== 'zh_CN' && targetLang !== 'en_US')) { + wx.showToast({ + title: '插件仅支持中英文互译', + icon: 'none' + }); + return; + } + + manager.start({ + lang: sourceLang, + trans_lang: targetLang, + }); + }, + + stopRecording: function () { + manager.stop(); + }, + + translate: function (text) { + const { sourceLang, targetLang } = this.data; + if (sourceLang === targetLang) { + this.setData({ outputText: text }); + return; + } + + this.setData({ outputText: '正在翻译...' }); + + plugin.translate({ + lfrom: sourceLang, + lto: targetLang, + content: text, + success: (res) => { + if (res.retcode === 0) { + this.setData({ outputText: res.result }); + } else { + console.error('翻译失败', res); + this.setData({ outputText: '翻译失败' }); + } + }, + fail: (err) => { + console.error('翻译接口调用失败', err); + this.setData({ outputText: '翻译出错' }); + } + }); + } +}); + +/* +--- Original index.v13.js content (commented out) --- +const plugin = requirePlugin('WechatSI'); +const manager = plugin.getRecordRecognitionManager(); // CRITICAL: Global creation + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' } + }, + langCodes: ['zh', 'en'], // Only CN and EN for this test version + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [] + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); // Set up event listeners for the globally created manager + }, + + // --- Language Selection Logic (Simplified for CN/EN only) --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Pure Plugin Mode) --- + initRecorderManager: function() { + // Event listeners are set on the globally created manager + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + // Only onStop is used for final results + manager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + // --- Recording Start/Stop (Pure Plugin Mode) --- + startRecording: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ isRecording: true }); + manager.start({ + lang: this.data.languages[sourceLang].code, + trans_lang: this.data.languages[targetLang].code, + }); + }, + + stopRecording: function () { + manager.stop(); + } +}); +*/ diff --git a/pages/index/index_v15_old.js b/pages/index/index_v15_old.js new file mode 100644 index 0000000000000000000000000000000000000000..e84fcb44960dd6ff1d5f559fe767f6469f1705e0 --- /dev/null +++ b/pages/index/index_v15_old.js @@ -0,0 +1,192 @@ +const plugin = requirePlugin("WechatSI"); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecordManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === sourceLang + })), + targetLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === targetLang + })) + }); + }, + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, this.initializeLanguages); + }, + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, this.initializeLanguages); + }, + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Hybrid Mode) --- + initRecordManager: function () { + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + manager.onRecognize = (res) => { + // For plugin mode, this updates transcript in real-time + // For HF mode, we only care about the final result in onStop + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + if (isChineseEnglish) { + this.setData({ transcript: res.result }); + } + }; + + manager.onStop = (res) => { + this.setData({ isRecording: false }); + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + if (isChineseEnglish) { + // Mode 1: Plugin handles both ASR and Translation + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + } else { + // Mode 2: Plugin handles ASR, then HF handles translation + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + // Use plugin for ASR and Translation + this.manager.start({ + lang: this.data.languages[sourceLang].code, + trans_lang: this.data.languages[targetLang].code, + }); + } else { + // Use plugin for ASR only, then HF for translation + this.manager.start({ + lang: this.data.languages[sourceLang].code, + // No trans_lang here, as HF will handle translation + }); + } + }, + + stopRecording: function () { + this.manager.stop(); + }, + + // --- HF Bridge Translation Flow (for non-CN/EN pairs) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + // Step 1: Translate source (e.g., JA) to English via HF + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + // Step 2: Translate English result to final target (e.g., ZH) via HF + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v16_old.js b/pages/index/index_v16_old.js new file mode 100644 index 0000000000000000000000000000000000000000..ef2ae79754213ffd13969a56c01de31414e2e8bc --- /dev/null +++ b/pages/index/index_v16_old.js @@ -0,0 +1,192 @@ +const plugin = requirePlugin("WechatSI"); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecordManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === sourceLang + })), + targetLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === targetLang + })) + }); + }, + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, this.initializeLanguages); + }, + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, this.initializeLanguages); + }, + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Hybrid Mode) --- + initRecordManager: function () { + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + manager.onRecognize = (res) => { + // For plugin mode, this updates transcript in real-time + // For HF mode, we only care about the final result in onStop + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + if (isChineseEnglish) { + this.setData({ transcript: res.result }); + } + }; + + manager.onStop = (res) => { + this.setData({ isRecording: false }); + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + if (isChineseEnglish) { + // Mode 1: Plugin handles both ASR and Translation + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + } else { + // Mode 2: Plugin handles ASR, then HF handles translation + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + // Use plugin for ASR and Translation + manager.start({ + lang: this.data.languages[sourceLang].code, + trans_lang: this.data.languages[targetLang].code, + }); + } else { + // Use plugin for ASR only, then HF for translation + manager.start({ + lang: this.data.languages[sourceLang].code, + // No trans_lang here, as HF will handle translation + }); + } + }, + + stopRecording: function () { + manager.stop(); + }, + + // --- HF Bridge Translation Flow (for non-CN/EN pairs) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + // Step 1: Translate source (e.g., JA) to English via HF + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + // Step 2: Translate English result to final target (e.g., ZH) via HF + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v17_old.js b/pages/index/index_v17_old.js new file mode 100644 index 0000000000000000000000000000000000000000..74a5e1983b196aa9009b6d0cd3f024e6c55c13e2 --- /dev/null +++ b/pages/index/index_v17_old.js @@ -0,0 +1,222 @@ +const plugin = requirePlugin("WechatSI"); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecordManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === sourceLang + })), + targetLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === targetLang + })) + }); + }, + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, this.initializeLanguages); + }, + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, this.initializeLanguages); + }, + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Hybrid Mode) --- + initRecordManager: function () { + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + manager.onRecognize = (res) => { + // For plugin mode, this updates transcript in real-time + // For HF mode, we only care about the final result in onStop + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + if (isChineseEnglish) { + this.setData({ transcript: res.result }); + } + }; + + manager.onStop = (res) => { + this.setData({ isRecording: false }); + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + if (isChineseEnglish) { + // Mode 1: Plugin handles ASR, then we explicitly call plugin.translate + if (res.result) { + this.setData({ transcript: res.result }); + this.translate(res.result); // CRITICAL: Explicitly call translate + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + } else { + // Mode 2: Plugin handles ASR, then HF handles translation + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + // Use plugin for ASR only (no trans_lang here), then explicitly call plugin.translate + manager.start({ + lang: this.data.languages[sourceLang].code, + // No trans_lang here, as we will call plugin.translate explicitly + }); + } else { + // Use plugin for ASR only, then HF for translation + manager.start({ + lang: this.data.languages[sourceLang].code, + // No trans_lang here, as HF will handle translation + }); + } + }, + + stopRecording: function () { + manager.stop(); + }, + + // --- CRITICAL: Re-introduced translate function for plugin mode --- + translate: function (text) { + const { sourceLang, targetLang } = this.data; + if (sourceLang === targetLang) { + this.setData({ outputText: text }); + return; + } + + this.setData({ outputText: '正在翻译...' }); + + plugin.translate({ + lfrom: this.data.languages[sourceLang].code, + lto: this.data.languages[targetLang].code, + content: text, + success: (res) => { + if (res.retcode === 0) { + this.setData({ outputText: res.result }); + } else { + console.error('翻译失败', res); + this.setData({ outputText: '翻译失败' }); + } + }, + fail: (err) => { + console.error('翻译接口调用失败', err); + this.setData({ outputText: '翻译出错' }); + } + }); + }, + + // --- HF Bridge Translation Flow (for non-CN/EN pairs) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + // Step 1: Translate source (e.g., JA) to English via HF + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + // Step 2: Translate English result to final target (e.g., ZH) via HF + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v18_old.js b/pages/index/index_v18_old.js new file mode 100644 index 0000000000000000000000000000000000000000..1c399786ee9ea823c9dfa71fbf09eaf2d55b78e9 --- /dev/null +++ b/pages/index/index_v18_old.js @@ -0,0 +1,231 @@ +const plugin = requirePlugin("WechatSI"); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecordManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === sourceLang + })), + targetLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === targetLang + })) + }); + }, + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, this.initializeLanguages); + }, + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, this.initializeLanguages); + }, + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Hybrid Mode) --- + initRecordManager: function () { + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + manager.onRecognize = (res) => { + // For plugin mode, this updates transcript in real-time + // For HF mode, we only care about the final result in onStop + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + if (isChineseEnglish) { + this.setData({ transcript: res.result }); + } + }; + + manager.onStop = (res) => { + this.setData({ isRecording: false }); + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + if (isChineseEnglish) { + // Mode 1: Plugin handles ASR, then we explicitly call plugin.translate + if (res.result) { + this.setData({ transcript: res.result }); + this.translate(res.result); // CRITICAL: Explicitly call translate + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + } else { + // Mode 2: Plugin handles ASR, then HF handles translation + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + // Use plugin for ASR only (no trans_lang here), then explicitly call plugin.translate + manager.start({ + lang: this.data.languages[sourceLang].code, + // No trans_lang here, as we will call plugin.translate explicitly + }); + } else { + // CRITICAL: Check if source language is supported by plugin for ASR + if (sourceLang !== 'zh' && sourceLang !== 'en') { + wx.showToast({ + title: '当前源语言不支持插件ASR', + icon: 'none' + }); + this.setData({ isRecording: false, transcript: '请选择中文或英文作为源语言' }); + return; + } + // Use plugin for ASR only, then HF for translation + manager.start({ + lang: this.data.languages[sourceLang].code, + // No trans_lang here, as HF will handle translation + }); + } + }, + + stopRecording: function () { + manager.stop(); + }, + + // --- CRITICAL: Re-introduced translate function for plugin mode --- + translate: function (text) { + const { sourceLang, targetLang } = this.data; + if (sourceLang === targetLang) { + this.setData({ outputText: text }); + return; + } + + this.setData({ outputText: '正在翻译...' }); + + plugin.translate({ + lfrom: this.data.languages[sourceLang].code, + lto: this.data.languages[targetLang].code, + content: text, + success: (res) => { + if (res.retcode === 0) { + this.setData({ outputText: res.result }); + } else { + console.error('翻译失败', res); + this.setData({ outputText: '翻译失败' }); + } + }, + fail: (err) => { + console.error('翻译接口调用失败', err); + this.setData({ outputText: '翻译出错' }); + } + }); + }, + + // --- HF Bridge Translation Flow (for non-CN/EN pairs) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + // Step 1: Translate source (e.g., JA) to English via HF + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + // Step 2: Translate English result to final target (e.g., ZH) via HF + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v19_old.js b/pages/index/index_v19_old.js new file mode 100644 index 0000000000000000000000000000000000000000..f58830a036c07f387c38ed67088df5b01f282a13 --- /dev/null +++ b/pages/index/index_v19_old.js @@ -0,0 +1,234 @@ +const plugin = requirePlugin("WechatSI"); +const pluginManager = plugin.getRecordRecognitionManager(); // For CN/EN ASR & Translation +const nativeRecorderManager = wx.getRecorderManager(); // For JA/KO ASR (recording only) + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentActiveManager: null // To track which manager is active + }, + + onLoad: function () { + this.initializeLanguages(); + this.initPluginManager(); + this.initNativeRecorderManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === sourceLang + })), + targetLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === targetLang + })) + }); + }, + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, this.initializeLanguages); + }, + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, this.initializeLanguages); + }, + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Plugin Manager Initialization (for CN/EN) --- + initPluginManager: function () { + pluginManager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + pluginManager.onRecognize = (res) => { + this.setData({ transcript: res.result }); + }; + + pluginManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.result) { + this.setData({ transcript: res.result }); + this.translate(res.result); // Explicitly call translate for plugin mode + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + }; + + pluginManager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + // --- Native Recorder Manager Initialization (for JA/KO recording) --- + initNativeRecorderManager: function () { + nativeRecorderManager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + nativeRecorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + }; + + nativeRecorderManager.onError = (res) => { + this.setData({ isRecording: false, transcript: '录音失败', outputText: res.errMsg }); + }; + }, + + // --- Unified Start Recording --- + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + this.setData({ currentActiveManager: 'plugin' }); + pluginManager.start({ + lang: this.data.languages[sourceLang].code, + trans_lang: this.data.languages[targetLang].code, + }); + } else { + this.setData({ currentActiveManager: 'native' }); + nativeRecorderManager.start({ + format: 'mp3', // Ensure format is compatible with HF backend + sampleRate: 16000, + numberOfChannels: 1, + encodeBitRate: 96000, + }); + } + }, + + // --- Unified Stop Recording --- + stopRecording: function () { + if (this.data.currentActiveManager === 'plugin') { + pluginManager.stop(); + } else if (this.data.currentActiveManager === 'native') { + nativeRecorderManager.stop(); + } + }, + + // --- Plugin Translation Function (for CN/EN) --- + translate: function (text) { + const { sourceLang, targetLang } = this.data; + if (sourceLang === targetLang) { + this.setData({ outputText: text }); + return; + } + + this.setData({ outputText: '正在翻译...' }); + + plugin.translate({ + lfrom: this.data.languages[sourceLang].code, + lto: this.data.languages[targetLang].code, + content: text, + success: (res) => { + if (res.retcode === 0) { + this.setData({ outputText: res.result }); + } else { + console.error('翻译失败', res); + this.setData({ outputText: '翻译失败' }); + } + }, + fail: (err) => { + console.error('翻译接口调用失败', err); + this.setData({ outputText: '翻译出错' }); + } + }); + }, + + // --- HF Bridge Translation Flow (for non-CN/EN pairs) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + // Step 1: Translate source (e.g., JA) to English via HF + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + // Step 2: Translate English result to final target (e.g., ZH) via HF + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v20_old.js b/pages/index/index_v20_old.js new file mode 100644 index 0000000000000000000000000000000000000000..13c2cd739993bb9acbe358cd85d286f292c5c938 --- /dev/null +++ b/pages/index/index_v20_old.js @@ -0,0 +1,179 @@ +const plugin = requirePlugin("WechatSI"); +const manager = plugin.getRecordRecognitionManager(); // Only one manager + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], // Use short codes for internal logic + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecordManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === sourceLang + })), + targetLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === targetLang + })) + }); + }, + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, this.initializeLanguages); + }, + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, this.initializeLanguages); + }, + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Single Manager Mode) --- + initRecordManager: function () { + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + manager.onRecognize = (res) => { + // Only update transcript for plugin-handled ASR (CN/EN) + const { sourceLang } = this.data; + if (sourceLang === 'zh' || sourceLang === 'en') { + this.setData({ transcript: res.result }); + } + }; + + manager.onStop = (res) => { + this.setData({ isRecording: false }); + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + if (isChineseEnglish) { + // Mode 1: Plugin handles both ASR and Translation + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + } else { + // Mode 2: Plugin handles ASR (if CN/EN), then HF handles translation + if (res.result) { // res.result is the ASR result from plugin + this.setData({ transcript: res.result }); + this.fullBackendBridge(res.result, this.data.sourceLang, this.data.targetLang); + } else { + this.setData({ transcript: '识别结果为空' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + // CRITICAL: Check if source language is supported by plugin for ASR + if (sourceLang !== 'zh' && sourceLang !== 'en') { + wx.showToast({ + title: '当前源语言不支持语音输入', + icon: 'none' + }); + this.setData({ isRecording: false, transcript: '请选择中文或英文作为源语言' }); + return; + } + + if (isChineseEnglish) { + // Use plugin for ASR and Translation + manager.start({ + lang: this.data.languages[sourceLang].code, + trans_lang: this.data.languages[targetLang].code, + }); + } else { + // Use plugin for ASR only, then HF for translation + manager.start({ + lang: this.data.languages[sourceLang].code, + // No trans_lang here, as HF will handle translation + }); + } + }, + + stopRecording: function () { + manager.stop(); + }, + + // --- HF Bridge Translation Flow (for non-CN/EN pairs) --- + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (1/2)..' }); // Simplified steps + // Step 1: Translate source (e.g., ZH) to English via HF + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + // Step 2: Translate English result to final target (e.g., JA) via HF + this.setData({ outputText: '翻译中 (2/2)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v21_old.js b/pages/index/index_v21_old.js new file mode 100644 index 0000000000000000000000000000000000000000..8d0357a6ad9e220dfb8afafd8c5171c0d7285957 --- /dev/null +++ b/pages/index/index_v21_old.js @@ -0,0 +1,177 @@ +const plugin = requirePlugin("WechatSI"); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + // This flag will determine if we need to call our backend + useHfBridge: false + }, + + onLoad: function () { + this.initializeLanguages(); + this.initManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === sourceLang + })), + targetLanguages: langCodes.map(c => ({ + langCode: c, + name: languages[c].name, + flag: languages[c].flag, + selected: c === targetLang + })) + }); + }, + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, this.initializeLanguages); + }, + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, this.initializeLanguages); + }, + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Unified Plugin Manager Initialization --- + initManager: function () { + manager.onStart = (res) => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + manager.onRecognize = (res) => { + // Live recognition feedback + this.setData({ transcript: res.result }); + }; + + manager.onStop = (res) => { + this.setData({ isRecording: false }); + + if (res.result) { + this.setData({ transcript: res.result }); + // If using HF Bridge, the plugin only provides the transcript. + // We then manually call our backend for translation. + if (this.data.useHfBridge) { + this.translateViaHfBridge(res.result); + } else { + // Otherwise, the plugin provides the translation directly. + this.setData({ outputText: res.translateResult || '翻译结果为空' }); + } + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + // --- Unified Start/Stop Recording --- + startRecording: function () { + const { sourceLang, targetLang, languages } = this.data; + + // Block recording if source is not CN or EN + if (sourceLang !== 'zh' && sourceLang !== 'en') { + wx.showToast({ + title: '语音输入暂仅支持中文和英文', + icon: 'none', + duration: 2000 + }); + return; + } + + const isChineseEnglishPair = (sourceLang === 'zh' || sourceLang === 'en') && (targetLang === 'zh' || targetLang === 'en'); + const useHfBridge = !isChineseEnglishPair; + + this.setData({ + isRecording: true, + useHfBridge: useHfBridge, + transcript: '', + outputText: '' + }); + + manager.start({ + lang: languages[sourceLang].code, + trans_lang: languages[targetLang].code, + // Critical: Only enable plugin's internal translation for direct CN/EN pairs + translate: !useHfBridge + }); + }, + + stopRecording: function () { + manager.stop(); + }, + + // --- HF Bridge Translation Flow --- + translateViaHfBridge: function(text) { + const { sourceLang, targetLang } = this.data; + this.setData({ outputText: '翻译中 (HF)...' }); + + // The plugin has already done the ASR (e.g., EN). + // Now we translate it to the final target (e.g., JA). + this.translateViaHF(text, sourceLang, targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + // Error message is set within translateViaHF + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { + "text": text, + "source_lang": sourceLang, + "target_lang": targetLang + }, + timeout: 45000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + const errorMsg = res.data.error || `HF翻译失败 (${sourceLang}->${targetLang})`; + this.setData({ outputText: errorMsg }); + callback(null); + } + }, + fail: (err) => { + this.setData({ outputText: `HF请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v22_old.js b/pages/index/index_v22_old.js new file mode 100644 index 0000000000000000000000000000000000000000..05a8dc988bb6f06692931c6ec6ddfa2f8aee76cf --- /dev/null +++ b/pages/index/index_v22_old.js @@ -0,0 +1,197 @@ +const plugin = requirePlugin("WechatSI"); +const pluginManager = plugin.getRecordRecognitionManager(); // For CN/EN ASR +const nativeRecorderManager = wx.getRecorderManager(); // For JA/KO Recording + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN', native: false }, + 'en': { name: 'English', flag: 'us', code: 'en_US', native: false }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', native: true }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', native: true } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentManager: null // 'plugin' or 'native' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initPluginManager(); + this.initNativeRecorderManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function (e) { + this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); + }, + selectTargetLanguage: function (e) { + this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); + }, + swapLanguages: function () { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Manager Initializations --- + initPluginManager: function () { + pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' }); + pluginManager.onRecognize = (res) => this.setData({ transcript: res.result }); + pluginManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (!res.result) { + this.setData({ transcript: '识别结果为空', outputText: '' }); + return; + } + this.setData({ transcript: res.result }); + const { sourceLang, targetLang } = this.data; + const isChineseEnglishPair = (sourceLang === 'zh' || sourceLang === 'en') && (targetLang === 'zh' || targetLang === 'en'); + + if (isChineseEnglishPair) { + this.translateViaPlugin(res.result); + } else { + this.translateViaHfBridge(res.result, sourceLang, targetLang); + } + }; + pluginManager.onError = (res) => this.setData({ isRecording: false, transcript: '插件识别失败', outputText: res.msg }); + }, + + initNativeRecorderManager: function () { + nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' }); + nativeRecorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + }; + nativeRecorderManager.onError = (res) => this.setData({ isRecording: false, transcript: '原生录音失败', outputText: res.errMsg }); + }, + + // --- Recording Logic --- + startRecording: function () { + const { sourceLang, languages } = this.data; + const shouldUseNative = languages[sourceLang].native; + + this.setData({ isRecording: true, currentManager: shouldUseNative ? 'native' : 'plugin' }); + + if (shouldUseNative) { + nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 }); + } else { + pluginManager.start({ lang: languages[sourceLang].code, translate: false }); + } + }, + + stopRecording: function () { + if (this.data.currentManager === 'native') { + nativeRecorderManager.stop(); + } else { + pluginManager.stop(); + } + }, + + // --- Translation Flows --- + translateViaPlugin: function(text) { + const { sourceLang, targetLang, languages } = this.data; + if (sourceLang === targetLang) { + this.setData({ outputText: text }); + return; + } + this.setData({ outputText: '翻译中 (插件)...' }); + plugin.translate({ + lfrom: languages[sourceLang].code, + lto: languages[targetLang].code, + content: text, + success: (res) => { + if (res.retcode === 0) { + this.setData({ outputText: res.result }); + } else { + this.setData({ outputText: '插件翻译失败' }); + } + }, + fail: () => this.setData({ outputText: '插件翻译接口调用失败' }) + }); + }, + + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (HF)...' }); + wx.getFileSystemManager().readFile({ + filePath, + encoding: 'base64', + success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { + this.setData({ transcript: 'HF识别失败', outputText: asrRes.data.error || '' }); + } + }, + fail: () => this.setData({ transcript: 'HF识别请求失败' }) + }); + }, + fail: () => this.setData({ transcript: '读取录音文件失败' }) + }); + }, + + translateViaHfBridge: function(text, source, target) { + this.setData({ outputText: '翻译中 (HF)...' }); + if (source === target) { + this.setData({ outputText: text }); + return; + } + this.translateViaHF(text, source, target, (result) => { + if (result) { + this.setData({ outputText: result }); + } + // Error message is set within translateViaHF + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 45000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + const errorMsg = res.data.error || `HF翻译失败`; + this.setData({ outputText: errorMsg }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败` }); + callback(null); + } + }); + } +}); diff --git a/pages/index/index_v24_old.js b/pages/index/index_v24_old.js new file mode 100644 index 0000000000000000000000000000000000000000..0041ef40a91c53340ad3d2884133a00354e82bc8 --- /dev/null +++ b/pages/index/index_v24_old.js @@ -0,0 +1,197 @@ +const plugin = requirePlugin("WechatSI"); +const pluginManager = plugin.getRecordRecognitionManager(); +const nativeRecorderManager = wx.getRecorderManager(); + +// Helper function to show detailed errors +function showDetailedError(title, content) { + wx.showModal({ + title: title, + content: typeof content === 'object' ? JSON.stringify(content) : String(content), + showCancel: false + }); +} + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true }, + 'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentManagerType: null // 'plugin' or 'native' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initPluginManager(); + this.initNativeRecorderManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: Object.keys(languages).map(key => ({ ...languages[key], langCode: key, selected: key === sourceLang })), + targetLanguages: Object.keys(languages).map(key => ({ ...languages[key], langCode: key, selected: key === targetLang })) + }); + }, + selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function () { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Manager Initializations with Detailed Error Popups --- + initPluginManager: function () { + pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' }); + pluginManager.onRecognize = (res) => this.setData({ transcript: res.result }); + pluginManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (!res.result) { return this.setData({ transcript: '识别结果为空' }); } + this.setData({ transcript: res.result }); + const { sourceLang, targetLang } = this.data; + const isPluginTarget = this.data.languages[targetLang].usePlugin; + if (isPluginTarget) { + this.translateViaPlugin(res.result); + } else { + this.translateViaHfBridge(res.result, sourceLang, targetLang); + } + }; + pluginManager.onError = (res) => { + this.setData({ isRecording: false }); + showDetailedError('插件录音失败', res); + }; + }, + + initNativeRecorderManager: function () { + nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' }); + nativeRecorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); } + else { this.setData({ transcript: '原生录音文件获取失败' }); } + }; + nativeRecorderManager.onError = (res) => { + this.setData({ isRecording: false }); + showDetailedError('原生录音失败', res); + }; + }, + + // --- Main Record Button Handler --- + handleRecordToggle: function() { + if (this.data.isRecording) { + this.stopRecording(); + return; + } + wx.getSetting({ + success: (res) => { + if (!res.authSetting['scope.record']) { + wx.authorize({ + scope: 'scope.record', + success: this.startRecording, + fail: (err) => showDetailedError('授权失败', err) + }); + } else { + this.startRecording(); + } + }, + fail: (err) => showDetailedError('无法获取权限设置', err) + }); + }, + + startRecording: function () { + // *** DEBUG LOGGING *** + console.log(`[DEBUG] Attempting to start recording. Source Language: ${this.data.sourceLang}`); + + const { sourceLang, languages } = this.data; + const shouldUsePlugin = languages[sourceLang].usePlugin; + const managerType = shouldUsePlugin ? 'plugin' : 'native'; + + this.setData({ isRecording: true, currentManagerType: managerType }); + + if (shouldUsePlugin) { + pluginManager.start({ lang: languages[sourceLang].code, translate: false }); + } else { + nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 }); + } + }, + + stopRecording: function () { + if (this.data.currentManagerType === 'native') { + nativeRecorderManager.stop(); + } else { + pluginManager.stop(); + } + }, + + // --- Translation Logic (unchanged) --- + translateViaPlugin: function(text) { + const { sourceLang, targetLang, languages } = this.data; + if (sourceLang === targetLang) { return this.setData({ outputText: text }); } + this.setData({ outputText: '翻译中 (插件)...' }); + plugin.translate({ + lfrom: languages[sourceLang].code, + lto: languages[targetLang].code, + content: text, + success: (res) => { + if (res.retcode === 0) { this.setData({ outputText: res.result }); } + else { showDetailedError('插件翻译失败', res); } + }, + fail: (err) => showDetailedError('插件翻译接口调用失败', err) + }); + }, + + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (HF)...' }); + wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { showDetailedError('HF识别失败', asrRes.data); } + }, + fail: (err) => showDetailedError('HF识别请求失败', err) + }); + }}); + }, + + translateViaHfBridge: function(text, source, target) { + if (source === target) { return this.setData({ outputText: text }); } + this.setData({ outputText: '翻译中 (HF)...' }); + this.translateViaHF(text, source, target, (result) => { + if (result) { this.setData({ outputText: result }); } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 45000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); } + else { showDetailedError('HF翻译失败', res.data); callback(null); } + }, + fail: (err) => { showDetailedError('HF翻译请求失败', err); callback(null); } + }); + } +}); diff --git a/pages/index/index_v25_old.js b/pages/index/index_v25_old.js new file mode 100644 index 0000000000000000000000000000000000000000..297e5bb539849795478500321d903c77192906a8 --- /dev/null +++ b/pages/index/index_v25_old.js @@ -0,0 +1,203 @@ +const plugin = requirePlugin("WechatSI"); +const pluginManager = plugin.getRecordRecognitionManager(); +const nativeRecorderManager = wx.getRecorderManager(); + +// Helper function to show detailed errors +function showDetailedError(title, content) { + wx.showModal({ + title: title, + content: typeof content === 'object' ? JSON.stringify(content) : String(content), + showCancel: false + }); +} + +Page({ + data: { + // Static language configuration + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true }, + 'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false } + }, + // Dynamic data + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentManagerType: null // 'plugin' or 'native' + }, + + onLoad: function () { + // Set the full language arrays for the UI to use + this.setData({ + sourceLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: this.getLangCode(lang.code)})), + targetLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: this.getLangCode(lang.code)})) + }); + this.initPluginManager(); + this.initNativeRecorderManager(); + }, + + getLangCode: function(fullCode) { + return fullCode.split('_')[0]; + }, + + // --- Language Selection Logic (Simplified and Synchronous) --- + selectSourceLanguage: function (e) { + this.setData({ sourceLang: e.currentTarget.dataset.langCode }); + }, + selectTargetLanguage: function (e) { + this.setData({ targetLang: e.currentTarget.dataset.langCode }); + }, + swapLanguages: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ + sourceLang: targetLang, + targetLang: sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }); + }, + + // --- Manager Initializations --- + initPluginManager: function () { + pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' }); + pluginManager.onRecognize = (res) => this.setData({ transcript: res.result }); + pluginManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (!res.result) { return this.setData({ transcript: '识别结果为空' }); } + this.setData({ transcript: res.result }); + const { sourceLang, targetLang, languages } = this.data; + const isPluginTarget = languages[targetLang].usePlugin; + if (sourceLang === targetLang) { + this.setData({ outputText: res.result }); + } else if (isPluginTarget) { + this.translateViaPlugin(res.result); + } else { + this.translateViaHfBridge(res.result, sourceLang, targetLang); + } + }; + pluginManager.onError = (res) => { + this.setData({ isRecording: false }); + showDetailedError('插件录音失败', res); + }; + }, + + initNativeRecorderManager: function () { + nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' }); + nativeRecorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); } + else { this.setData({ transcript: '原生录音文件获取失败' }); } + }; + nativeRecorderManager.onError = (res) => { + this.setData({ isRecording: false }); + showDetailedError('原生录音失败', res); + }; + }, + + // --- Main Record Button Handler --- + handleRecordToggle: function() { + if (this.data.isRecording) { + this.stopRecording(); + return; + } + wx.getSetting({ + success: (res) => { + if (!res.authSetting['scope.record']) { + wx.authorize({ + scope: 'scope.record', + success: this.startRecording, + fail: (err) => showDetailedError('授权失败', err) + }); + } else { + this.startRecording(); + } + }, + fail: (err) => showDetailedError('无法获取权限设置', err) + }); + }, + + startRecording: function () { + console.log(`[DEBUG] Starting recording. Source: ${this.data.sourceLang}, Target: ${this.data.targetLang}`); + const { sourceLang, languages } = this.data; + const shouldUsePlugin = languages[sourceLang].usePlugin; + const managerType = shouldUsePlugin ? 'plugin' : 'native'; + + this.setData({ isRecording: true, currentManagerType: managerType }); + + if (shouldUsePlugin) { + pluginManager.start({ lang: languages[sourceLang].code, translate: false }); + } else { + nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 }); + } + }, + + stopRecording: function () { + if (this.data.currentManagerType === 'native') { + nativeRecorderManager.stop(); + } else { + pluginManager.stop(); + } + }, + + // --- Translation Logic --- + translateViaPlugin: function(text) { + const { sourceLang, targetLang, languages } = this.data; + this.setData({ outputText: '翻译中 (插件)...' }); + plugin.translate({ + lfrom: languages[sourceLang].code, + lto: languages[targetLang].code, + content: text, + success: (res) => { + if (res.retcode === 0) { this.setData({ outputText: res.result }); } + else { showDetailedError('插件翻译失败', res); } + }, + fail: (err) => showDetailedError('插件翻译接口调用失败', err) + }); + }, + + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (HF)...' }); + wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { showDetailedError('HF识别失败', asrRes.data); } + }, + fail: (err) => showDetailedError('HF识别请求失败', err) + }); + }}); + }, + + translateViaHfBridge: function(text, source, target) { + if (source === target) { return this.setData({ outputText: text }); } + this.setData({ outputText: '翻译中 (HF)...' }); + this.translateViaHF(text, source, target, (result) => { + if (result) { this.setData({ outputText: result }); } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 45000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); } + else { showDetailedError('HF翻译失败', res.data); callback(null); } + }, + fail: (err) => { showDetailedError('HF翻译请求失败', err); callback(null); } + }); + } +}); diff --git a/pages/index/index_v26_old.js b/pages/index/index_v26_old.js new file mode 100644 index 0000000000000000000000000000000000000000..c46c861ae556dec75310c55a4652f32229af3428 --- /dev/null +++ b/pages/index/index_v26_old.js @@ -0,0 +1,150 @@ +const plugin = requirePlugin("WechatSI"); +const pluginManager = plugin.getRecordRecognitionManager(); +const nativeRecorderManager = wx.getRecorderManager(); + +function showDetailedError(title, content) { + wx.showModal({ + title: title, + content: typeof content === 'object' ? JSON.stringify(content) : String(content), + showCancel: false + }); +} + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true }, + 'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false } + }, + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + }, + + onLoad: function () { + this.setData({ + sourceLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: lang.code.split('_')[0]})), + targetLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: lang.code.split('_')[0]})) + }); + this.initPluginManager(); + this.initNativeRecorderManager(); + }, + + // --- Language Selection & UI --- + selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }); }, + selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }); }, + swapLanguages: function () { + this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript }); + }, + + // --- Manager Initializations --- + initPluginManager: function () { + pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' }); + pluginManager.onRecognize = (res) => this.setData({ transcript: res.result }); + pluginManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (!res.result) { return this.setData({ transcript: '识别结果为空' }); } + this.setData({ transcript: res.result }); + const { sourceLang, targetLang, languages } = this.data; + if (sourceLang === targetLang) { return this.setData({ outputText: res.result }); } + const isPluginTarget = languages[targetLang].usePlugin; + if (isPluginTarget) { this.translateViaPlugin(res.result); } + else { this.translateViaHfBridge(res.result, sourceLang, targetLang); } + }; + pluginManager.onError = (res) => { this.setData({ isRecording: false }); showDetailedError('插件录音失败', res); }; + }, + + initNativeRecorderManager: function () { + nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' }); + nativeRecorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); } + else { this.setData({ transcript: '原生录音文件获取失败' }); } + }; + nativeRecorderManager.onError = (res) => { this.setData({ isRecording: false }); showDetailedError('原生录音失败', res); }; + }, + + // --- Main Record Button Handler --- + handleRecordToggle: function() { + if (this.data.isRecording) { this.stopRecording(); return; } + wx.getSetting({ + success: (res) => { + if (!res.authSetting['scope.record']) { + wx.authorize({ scope: 'scope.record', success: this.startRecording, fail: (err) => showDetailedError('授权失败', err) }); + } else { this.startRecording(); } + }, + fail: (err) => showDetailedError('无法获取权限设置', err) + }); + }, + + startRecording: function () { + const { sourceLang, languages } = this.data; + const langConfig = languages[sourceLang]; + const shouldUsePlugin = langConfig ? langConfig.usePlugin : false; + + // *** ULTIMATE DEBUG LOG *** + console.log(`[DEBUG] Start decision: sourceLang=${sourceLang}, langConfig=${JSON.stringify(langConfig)}, shouldUsePlugin=${shouldUsePlugin}`); + + this.setData({ isRecording: true }); + + if (shouldUsePlugin) { + pluginManager.start({ lang: langConfig.code, translate: false }); + } else { + nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 }); + } + }, + + // --- ROBUST Stop Recording --- + stopRecording: function () { + this.setData({ isRecording: false }); + try { pluginManager.stop(); } catch (e) { console.log("Plugin manager wasn't running or failed to stop."); } + try { nativeRecorderManager.stop(); } catch (e) { console.log("Native manager wasn't running or failed to stop."); } + }, + + // --- Translation Logic --- + translateViaPlugin: function(text) { + const { sourceLang, targetLang, languages } = this.data; + this.setData({ outputText: '翻译中 (插件)...' }); + plugin.translate({ + lfrom: languages[sourceLang].code, lto: languages[targetLang].code, content: text, + success: (res) => { if (res.retcode === 0) { this.setData({ outputText: res.result }); } else { showDetailedError('插件翻译失败', res); } }, + fail: (err) => showDetailedError('插件翻译接口调用失败', err) + }); + }, + + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (HF)...' }); + wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, method: 'POST', data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { showDetailedError('HF识别失败', asrRes.data); } + }, + fail: (err) => showDetailedError('HF识别请求失败', err) + }); + }}); + }, + + translateViaHfBridge: function(text, source, target) { + if (source === target) { return this.setData({ outputText: text }); } + this.setData({ outputText: '翻译中 (HF)...' }); + this.translateViaHF(text, source, target, (result) => { if (result) { this.setData({ outputText: result }); } }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, method: 'POST', data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, timeout: 45000, + success: (res) => { if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); } else { showDetailedError('HF翻译失败', res.data); callback(null); } }, + fail: (err) => { showDetailedError('HF翻译请求失败', err); callback(null); } + }); + } +}); diff --git a/pages/index/index_v27_old.js b/pages/index/index_v27_old.js new file mode 100644 index 0000000000000000000000000000000000000000..dfa7433b20a674fae8aed23e0cba2b4ce38deca5 --- /dev/null +++ b/pages/index/index_v27_old.js @@ -0,0 +1,159 @@ +const plugin = requirePlugin("WechatSI"); +const pluginManager = plugin.getRecordRecognitionManager(); +const nativeRecorderManager = wx.getRecorderManager(); + +function showDetailedError(title, content) { + wx.showModal({ + title: title, + content: typeof content === 'object' ? JSON.stringify(content) : String(content), + showCancel: false + }); +} + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true }, + 'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false } + }, + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + }, + + onLoad: function () { + this.setData({ + sourceLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: lang.code.split('_')[0]})), + targetLanguages: Object.values(this.data.languages).map(lang => ({...lang, langCode: lang.code.split('_')[0]})) + }); + this.initPluginManager(); + this.initNativeRecorderManager(); + }, + + // --- Language Selection & UI with DEBUG --- + selectSourceLanguage: function (e) { + const newLang = e.currentTarget.dataset.langCode; + console.log(`[DEBUG] Language button tapped. Changing sourceLang to: ${newLang}`); + this.setData({ sourceLang: newLang }); + }, + selectTargetLanguage: function (e) { + const newLang = e.currentTarget.dataset.langCode; + console.log(`[DEBUG] Language button tapped. Changing targetLang to: ${newLang}`); + this.setData({ targetLang: newLang }); + }, + swapLanguages: function () { + this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript }); + }, + + // --- Manager Initializations --- + initPluginManager: function () { + pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' }); + pluginManager.onRecognize = (res) => this.setData({ transcript: res.result }); + pluginManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (!res.result) { return this.setData({ transcript: '识别结果为空' }); } + this.setData({ transcript: res.result }); + const { sourceLang, targetLang, languages } = this.data; + if (sourceLang === targetLang) { return this.setData({ outputText: res.result }); } + const isPluginTarget = languages[targetLang].usePlugin; + if (isPluginTarget) { this.translateViaPlugin(res.result); } + else { this.translateViaHfBridge(res.result, sourceLang, targetLang); } + }; + pluginManager.onError = (res) => { this.setData({ isRecording: false }); showDetailedError('插件录音失败', res); }; + }, + + initNativeRecorderManager: function () { + nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' }); + nativeRecorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); } + else { this.setData({ transcript: '原生录音文件获取失败' }); } + }; + nativeRecorderManager.onError = (res) => { this.setData({ isRecording: false }); showDetailedError('原生录音失败', res); }; + }, + + // --- Main Record Button Handler --- + handleRecordToggle: function() { + if (this.data.isRecording) { this.stopRecording(); return; } + wx.getSetting({ + success: (res) => { + if (!res.authSetting['scope.record']) { + wx.authorize({ scope: 'scope.record', success: this.startRecording, fail: (err) => showDetailedError('授权失败', err) }); + } else { this.startRecording(); } + }, + fail: (err) => showDetailedError('无法获取权限设置', err) + }); + }, + + startRecording: function () { + const { sourceLang, languages } = this.data; + const langConfig = languages[sourceLang]; + // Fallback to a default config if something goes wrong, to prevent crashes + const shouldUsePlugin = langConfig ? langConfig.usePlugin : false; + + // *** ULTIMATE DEBUG LOG *** + console.log(`[DEBUG] Start decision: sourceLang=${sourceLang}, langConfig=${JSON.stringify(langConfig)}, shouldUsePlugin=${shouldUsePlugin}`); + + this.setData({ isRecording: true }); + + if (shouldUsePlugin) { + pluginManager.start({ lang: (langConfig || {code: 'zh_CN'}).code, translate: false }); + } else { + nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 }); + } + }, + + // --- ROBUST Stop Recording --- + stopRecording: function () { + this.setData({ isRecording: false }); + try { pluginManager.stop(); } catch (e) { /* Ignore error */ } + try { nativeRecorderManager.stop(); } catch (e) { /* Ignore error */ } + }, + + // --- Translation Logic --- + translateViaPlugin: function(text) { + const { sourceLang, targetLang, languages } = this.data; + this.setData({ outputText: '翻译中 (插件)...' }); + plugin.translate({ + lfrom: languages[sourceLang].code, lto: languages[targetLang].code, content: text, + success: (res) => { if (res.retcode === 0) { this.setData({ outputText: res.result }); } else { showDetailedError('插件翻译失败', res); } }, + fail: (err) => showDetailedError('插件翻译接口调用失败', err) + }); + }, + + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (HF)...' }); + wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, method: 'POST', data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { showDetailedError('HF识别失败', asrRes.data); } + }, + fail: (err) => showDetailedError('HF识别请求失败', err) + }); + }}); + }, + + translateViaHfBridge: function(text, source, target) { + if (source === target) { return this.setData({ outputText: text }); } + this.setData({ outputText: '翻译中 (HF)...' }); + this.translateViaHF(text, source, target, (result) => { if (result) { this.setData({ outputText: result }); } }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, method: 'POST', data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, timeout: 45000, + success: (res) => { if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); } else { showDetailedError('HF翻译失败', res.data); callback(null); } }, + fail: (err) => { showDetailedError('HF翻译请求失败', err); callback(null); } + }); + } +}); diff --git a/pages/index/index_v28_old.js b/pages/index/index_v28_old.js new file mode 100644 index 0000000000000000000000000000000000000000..d3d89487a1e9645696c68c5ae79677ce1c36c06f --- /dev/null +++ b/pages/index/index_v28_old.js @@ -0,0 +1,139 @@ +// FINAL VERSION: v28 - Unified Native Recording and HF Backend +const nativeRecorderManager = wx.getRecorderManager(); + +// Helper function to show detailed errors +function showDetailedError(title, content) { + wx.showModal({ + title: title, + content: typeof content === 'object' ? JSON.stringify(content) : String(content), + showCancel: false + }); +} + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn' }, + 'en': { name: 'English', flag: 'us' }, + 'ja': { name: '日本語', flag: 'jp' }, + 'ko': { name: '한국어', flag: 'kr' } + }, + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + }, + + onLoad: function () { + // Directly create language arrays for the UI from the static config + this.setData({ + sourceLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key })), + targetLanguages: Object.keys(this.data.languages).map(key => ({ ...this.data.languages[key], langCode: key })) + }); + this.initNativeRecorderManager(); + }, + + // --- Language Selection & UI --- + selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }); }, + selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }); }, + swapLanguages: function () { + this.setData({ sourceLang: this.data.targetLang, targetLang: this.data.sourceLang, transcript: this.data.outputText, outputText: this.data.transcript }); + }, + + // --- Unified Native Recorder Initialization --- + initNativeRecorderManager: function () { + nativeRecorderManager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + nativeRecorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { + // ALL recordings go to the HF backend for ASR + this.uploadAudioForASR(res.tempFilePath); + } else { + showDetailedError('录音失败', '未能获取到有效的录音文件路径。'); + } + }; + nativeRecorderManager.onError = (res) => { + this.setData({ isRecording: false }); + showDetailedError('录音发生错误', res); + }; + }, + + // --- Main Record Button Handler --- + handleRecordToggle: function() { + if (this.data.isRecording) { + this.stopRecording(); + return; + } + // Check permissions before starting + wx.getSetting({ + success: (res) => { + if (!res.authSetting['scope.record']) { + wx.authorize({ scope: 'scope.record', success: this.startRecording, fail: (err) => showDetailedError('授权失败', err) }); + } else { + this.startRecording(); + } + }, + fail: (err) => showDetailedError('无法获取权限设置', err) + }); + }, + + // --- Unified Start/Stop Recording --- + startRecording: function () { + this.setData({ isRecording: true }); + nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 }); + }, + + stopRecording: function () { + this.setData({ isRecording: false }); + nativeRecorderManager.stop(); + }, + + // --- Unified Backend ASR & Translation Flow --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/2)...' }); + wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + // After ASR, ALL translations go to the HF backend + this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { + showDetailedError('语音识别失败', asrRes.data); + } + }, + fail: (err) => showDetailedError('识别请求失败', err) + }); + }}); + }, + + translateViaHfBridge: function(text, source, target) { + if (source === target) { + return this.setData({ outputText: text }); + } + this.setData({ outputText: '正在翻译 (2/2)...' }); + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + data: { "text": text, "source_lang": source, "target_lang": target }, + timeout: 45000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + this.setData({ outputText: res.data.translated_text }); + } else { + showDetailedError('翻译失败', res.data); + } + }, + fail: (err) => showDetailedError('翻译请求失败', err) + }); + } +}); diff --git a/pages/index/index_v2_old.js b/pages/index/index_v2_old.js new file mode 100644 index 0000000000000000000000000000000000000000..4a7014b0e58a620134f6b7ebbedf784872cf463e --- /dev/null +++ b/pages/index/index_v2_old.js @@ -0,0 +1,232 @@ +const plugin = requirePlugin('WechatSI'); +const innerAudioContext = wx.createInnerAudioContext(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.recorderManager = wx.getRecorderManager(); + this.initRecorderManager(); + plugin.getRecordRecognitionManager = wx.getRecordRecognitionManager; + }, + + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + const sourceLanguages = langCodes.map(code => ({ + langCode: code, + name: languages[code].name, + flag: languages[code].flag, + selected: code === sourceLang + })); + const targetLanguages = langCodes.map(code => ({ + langCode: code, + name: languages[code].name, + flag: languages[code].flag, + selected: code === targetLang + })); + this.setData({ sourceLanguages, targetLanguages }); + }, + + selectSourceLanguage: function (e) { + const newSourceLang = e.currentTarget.dataset.langCode; + this.setData({ sourceLang: newSourceLang }, () => { + this.initializeLanguages(); + if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') { + this.translate(this.data.transcript); + } + }); + }, + + selectTargetLanguage: function (e) { + const newTargetLang = e.currentTarget.dataset.langCode; + this.setData({ targetLang: newTargetLang }, () => { + this.initializeLanguages(); + if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') { + this.translate(this.data.transcript); + } + }); + }, + + swapLanguages: function () { + let { sourceLang, targetLang, transcript, outputText } = this.data; + const tempLang = sourceLang; + sourceLang = targetLang; + targetLang = tempLang; + const tempText = transcript; + transcript = outputText; + outputText = tempText; + this.setData({ sourceLang, targetLang, transcript, outputText }, () => { + this.initializeLanguages(); + if (this.data.transcript.trim() && this.data.transcript !== '正在聆听...' && this.data.transcript !== '未能识别到语音') { + this.translate(this.data.transcript); + } + }); + }, + + initRecorderManager: function () { + this.recorderManager.onStart(() => { + this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' }); + }); + + this.recorderManager.onStop((res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件创建失败' }); + } + }); + + this.recorderManager.onError(() => { + this.setData({ isRecording: false, transcript: '语音识别出错' }); + }); + }, + + startRecording: function () { + this.recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' }); + }, + + stopRecording: function () { + this.recorderManager.stop(); + }, + + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ + filePath: filePath, + encoding: 'base64', + success: (res) => { + const base64Data = res.data; + const dataUri = 'data:audio/mp3;base64,' + base64Data; + + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": dataUri }, + timeout: 60000, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.transcript) { + const transcript = res.data.transcript; + this.setData({ transcript: transcript }); + this.translate(transcript); + } else { + this.setData({ transcript: '语音识别失败' }); + } + }, + fail: () => { + this.setData({ transcript: '语音识别请求失败' }); + } + }); + }, + fail: () => { + this.setData({ transcript: '读取音频文件失败' }); + } + }); + }, + + translate: function (text) { + if (!text) return; + const { sourceLang, targetLang, languages } = this.data; + + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + if (isChineseEnglish) { + // --- Scenario 1: Direct CN-EN translation using WeChat Plugin --- + this.setData({ outputText: '正在翻译 (微信)...' }); + plugin.translate({ + lfrom: languages[sourceLang].code, + lto: languages[targetLang].code, + content: text, + success: (res) => { + if (res.retcode == 0) { + this.setData({ outputText: res.result }); + } else { + this.setData({ outputText: '微信翻译失败' }); + } + }, + fail: () => { + this.setData({ outputText: '微信翻译接口调用失败' }); + } + }); + } else { + // --- Scenario 2: Hybrid translation for other languages --- + this.hybridTranslate(text, sourceLang, targetLang); + } + }, + + hybridTranslate: function(text, sourceLang, targetLang) { + // If source is not English, first translate to English using WeChat Plugin + if (sourceLang !== 'en') { + this.setData({ outputText: '翻译中 (1/2)...' }); + plugin.translate({ + lfrom: this.data.languages[sourceLang].code, + lto: 'en_US', + content: text, + success: (res) => { + if (res.retcode == 0) { + const englishText = res.result; + // If the final target is English, we are done. + if (targetLang === 'en') { + this.setData({ outputText: englishText }); + } else { + // Otherwise, send the high-quality English text to HF for the second leg + this.translateViaHF(englishText, 'en', targetLang); + } + } else { + this.setData({ outputText: '混合翻译失败 (1/2)' }); + } + }, + fail: () => { + this.setData({ outputText: '混合翻译接口调用失败 (1/2)' }); + } + }); + } else { + // If source is already English, directly call HF for translation + this.translateViaHF(text, sourceLang, targetLang); + } + }, + + translateViaHF: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/2)..' }); + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { + "text": text, + "source_lang": sourceLang, + "target_lang": targetLang + }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.translated_text) { + this.setData({ outputText: res.data.translated_text }); + } else { + this.setData({ outputText: '混合翻译失败 (2/2)' }); + } + }, + fail: () => { + this.setData({ outputText: '混合翻译出错 (2/2)' }); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v3_old.js b/pages/index/index_v3_old.js new file mode 100644 index 0000000000000000000000000000000000000000..beedf94b273309c3d6cf051c86d7442f7ca13e9b --- /dev/null +++ b/pages/index/index_v3_old.js @@ -0,0 +1,173 @@ +const plugin = requirePlugin('WechatSI'); +const innerAudioContext = wx.createInnerAudioContext(); + +// Get the voice recognition manager from the plugin +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentRecordMode: '' // 'plugin' or 'custom' + }, + + onLoad: function () { + this.initializeLanguages(); + // Custom recorder (for HF) + this.customRecorderManager = wx.getRecorderManager(); + this.initCustomRecorderManager(); + // Plugin recorder (for CN/EN) + this.initPluginRecorderManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recording Logic --- + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' }); + + if (isChineseEnglish) { + // Use the ultra-fast WeChat Plugin for both ASR and Translation + this.setData({ currentRecordMode: 'plugin' }); + manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code }); + } else { + // Use our custom HF backend for ASR for other languages + this.setData({ currentRecordMode: 'custom' }); + this.customRecorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' }); + } + }, + + stopRecording: function () { + if (this.data.currentRecordMode === 'plugin') { + manager.stop(); + } else if (this.data.currentRecordMode === 'custom') { + this.customRecorderManager.stop(); + } + this.setData({ isRecording: false }); + }, + + // --- Result Handlers --- + initPluginRecorderManager: function() { + manager.onStart = (res) => { + this.setData({ transcript: '正在识别和翻译...' }); + }; + manager.onRecognize = (res) => { + this.setData({ transcript: res.result }); + }; + manager.onStop = (res) => { + this.setData({ transcript: res.result, outputText: res.translateResult }); + }; + manager.onError = (res) => { + this.setData({ transcript: '微信插件识别失败', outputText: res.msg }); + }; + }, + + initCustomRecorderManager: function() { + this.customRecorderManager.onStop = (res) => { + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件创建失败' }); + } + }; + this.customRecorderManager.onError = () => { + this.setData({ transcript: '语音识别出错' }); + }; + }, + + // --- Custom Translation Flow (for non-CN/EN pairs) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (HF)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.hybridTranslate(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + hybridTranslate: function(text, sourceLang, targetLang) { + // If source is ZH or EN, use the fast plugin for the first leg of the bridge + if (sourceLang === 'zh' || sourceLang === 'en') { + this.setData({ outputText: '翻译中 (1/2)...' }); + plugin.translate({ lfrom: this.data.languages[sourceLang].code, lto: 'en_US', content: text, success: (res) => { + if (res.retcode === 0) { + this.translateViaHF(res.result, 'en', targetLang); + } else { this.setData({ outputText: '混合翻译失败 (1/2)' }); } + }}); + } else { + // If source is JA or KO, we MUST use our HF backend for the first leg + this.setData({ outputText: '翻译中 (1/2 - HF)..' }); + this.translateViaHF(text, sourceLang, 'en', (englishText) => { + // This is a callback to run after the first leg is complete + this.translateViaHF(englishText, 'en', targetLang); + }); + } + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + this.setData({ outputText: this.data.outputText + ' (2/2)..' }); + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + if (callback) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: res.data.translated_text }); + } + } else { this.setData({ outputText: 'HF翻译失败' }); } + }, + fail: () => { this.setData({ outputText: 'HF翻译请求失败' }); } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v4_old.js b/pages/index/index_v4_old.js new file mode 100644 index 0000000000000000000000000000000000000000..6d91272a8bad1833797b5c0af43119ae9b30fecc --- /dev/null +++ b/pages/index/index_v4_old.js @@ -0,0 +1,169 @@ +const plugin = requirePlugin('WechatSI'); +const innerAudioContext = wx.createInnerAudioContext(); +const pluginManager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentRecordMode: '' // 'plugin' or 'custom' + }, + + onLoad: function () { + this.initializeLanguages(); + this.customRecorderManager = wx.getRecorderManager(); + this.initCustomRecorderManager(); + this.initPluginRecorderManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recording Logic --- + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' }); + + if (isChineseEnglish) { + this.setData({ currentRecordMode: 'plugin' }); + pluginManager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code }); + } else { + this.setData({ currentRecordMode: 'custom' }); + this.customRecorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' }); + } + }, + + stopRecording: function () { + if (this.data.currentRecordMode === 'plugin') { + pluginManager.stop(); + } else if (this.data.currentRecordMode === 'custom') { + this.customRecorderManager.stop(); + } + this.setData({ isRecording: false }); + }, + + // --- Result Handlers --- + initPluginRecorderManager: function() { + pluginManager.onRecognize = (res) => { + this.setData({ transcript: res.result }); + }; + // CORRECTED: Use onTranslate to get translation results + pluginManager.onTranslate = (res) => { + this.setData({ outputText: res.result }); + }; + pluginManager.onStop = (res) => { + this.setData({ transcript: res.result, outputText: res.translateResult }); + }; + pluginManager.onError = (res) => { + this.setData({ transcript: '微信插件识别失败', outputText: res.msg }); + }; + }, + + initCustomRecorderManager: function() { + this.customRecorderManager.onStop = (res) => { + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { this.setData({ transcript: '录音文件创建失败' }); } + }; + this.customRecorderManager.onError = () => { + this.setData({ transcript: '语音识别出错' }); + }; + }, + + // --- Custom Translation Flow (for non-CN/EN pairs) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + // Start the full-backend translation bridge + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + // Step 1: Translate source (e.g., JA) to English via HF + this.setData({ outputText: '翻译中 (2/3)..' }); + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + // Step 2: Translate English result to final target (e.g., ZH) via HF + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + if (callback) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: res.data.translated_text }); + } + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + if (callback) callback(null); // Signal failure + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + if (callback) callback(null); // Signal failure + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v5_old.js b/pages/index/index_v5_old.js new file mode 100644 index 0000000000000000000000000000000000000000..ae94c2fe4d1b6477b938734df488988cc939d750 --- /dev/null +++ b/pages/index/index_v5_old.js @@ -0,0 +1,162 @@ +const plugin = requirePlugin('WechatSI'); +const innerAudioContext = wx.createInnerAudioContext(); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentMode: '' // 'plugin-translate' or 'hf-bridge' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Unified Recording Logic --- + initRecorderManager: function() { + manager.onStart = (res) => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + manager.onRecognize = (res) => { + this.setData({ transcript: res.result }); + }; + + manager.onTranslate = (res) => { + this.setData({ outputText: res.result }); + }; + + manager.onStop = (res) => { + this.setData({ isRecording: false }); + if (this.data.currentMode === 'plugin-translate') { + this.setData({ transcript: res.result, outputText: res.translateResult }); + } else if (this.data.currentMode === 'hf-bridge') { + // For HF mode, we need the audio file to upload. + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + // Mode 1: Fast, all-in-one ASR and Translation by the plugin + this.setData({ currentMode: 'plugin-translate' }); + manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code }); + } else { + // Mode 2: HF Bridge. Use plugin for ASR only, then our backend for translation. + this.setData({ currentMode: 'hf-bridge' }); + // We ask the plugin to record, but NOT to translate. + manager.start({ lang: this.data.languages[sourceLang].code, translate: false }); + } + }, + + stopRecording: function () { + manager.stop(); + }, + + // --- HF Bridge Translation Flow --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v6_old.js b/pages/index/index_v6_old.js new file mode 100644 index 0000000000000000000000000000000000000000..64b70554abfcff1064a22b1fdfaaaba3a5e72d32 --- /dev/null +++ b/pages/index/index_v6_old.js @@ -0,0 +1,150 @@ +const plugin = requirePlugin('WechatSI'); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentMode: '' // 'plugin-translate' or 'hf-bridge' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Unified, Simplified, and Correct Recording Logic --- + initRecorderManager: function() { + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + // CORRECTED: Only use onStop for final results to prevent race conditions. + manager.onStop = (res) => { + this.setData({ isRecording: false }); + if (this.data.currentMode === 'plugin-translate') { + this.setData({ transcript: res.result, outputText: res.translateResult }); + } else if (this.data.currentMode === 'hf-bridge') { + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + this.setData({ currentMode: 'plugin-translate' }); + manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code }); + } else { + this.setData({ currentMode: 'hf-bridge' }); + manager.start({ lang: this.data.languages[sourceLang].code, translate: false }); + } + }, + + stopRecording: function () { + manager.stop(); + }, + + // --- HF Bridge Translation Flow --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v7_old.js b/pages/index/index_v7_old.js new file mode 100644 index 0000000000000000000000000000000000000000..6d231502219f3cf5dd7e7697c50938d374487f32 --- /dev/null +++ b/pages/index/index_v7_old.js @@ -0,0 +1,157 @@ +const plugin = requirePlugin('WechatSI'); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP' }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentMode: '' // 'plugin-translate' or 'hf-bridge' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); // Correctly initialize the manager within the page lifecycle + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- CORRECTED: Unified, Lifecycle-Aware Recording Logic --- + initRecorderManager: function() { + const manager = plugin.getRecordRecognitionManager(); + + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + manager.onStop = (res) => { + this.setData({ isRecording: false }); + if (this.data.currentMode === 'plugin-translate') { + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + } else if (this.data.currentMode === 'hf-bridge') { + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + + // CRITICAL: Save the manager instance to the page context + this.manager = manager; + }, + + startRecording: function () { + const { sourceLang, targetLang } = this.data; + const isChineseEnglish = (sourceLang === 'zh' && targetLang === 'en') || (sourceLang === 'en' && targetLang === 'zh'); + + this.setData({ isRecording: true }); + + if (isChineseEnglish) { + this.setData({ currentMode: 'plugin-translate' }); + this.manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code }); + } else { + this.setData({ currentMode: 'hf-bridge' }); + this.manager.start({ lang: this.data.languages[sourceLang].code, translate: false }); + } + }, + + stopRecording: function () { + this.manager.stop(); + }, + + // --- HF Bridge Translation Flow (This part remains the same) --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + this.setData({ outputText: '翻译中 (2/3)..' }); + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v8_old.js b/pages/index/index_v8_old.js new file mode 100644 index 0000000000000000000000000000000000000000..aa2cf5cb3e9e4fa57fda33f1c429c31d349519df --- /dev/null +++ b/pages/index/index_v8_old.js @@ -0,0 +1,153 @@ +const recorderManager = wx.getRecorderManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn' }, + 'en': { name: 'English', flag: 'us' }, + 'ja': { name: '日本語', flag: 'jp' }, + 'ko': { name: '한국어', flag: 'kr' } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [], + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); + }, + + // --- Language Selection Logic --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Simplified, Unified, and Correct Recording Logic --- + initRecorderManager: function() { + recorderManager.onStart = () => { + this.setData({ isRecording: true, transcript: '正在聆听...', outputText: '' }); + }; + + recorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { + this.uploadAudioForASR(res.tempFilePath); + } else { + this.setData({ transcript: '录音文件获取失败' }); + } + }; + + recorderManager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + startRecording: function () { + recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 96000, format: 'mp3' }); + }, + + stopRecording: function () { + recorderManager.stop(); + }, + + // --- Unified Backend Flow --- + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (1/3)...' }); + const fileSystemManager = wx.getFileSystemManager(); + fileSystemManager.readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + // If source and target are the same, no need to translate + if (this.data.sourceLang === this.data.targetLang) { + this.setData({ outputText: transcript }); + return; + } + this.fullBackendBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败' }); } + }, + fail: () => { this.setData({ transcript: 'HF识别请求失败' }); } + }); + }}); + }, + + fullBackendBridge: function(text, sourceLang, targetLang) { + // If source is not English, bridge through English + if (sourceLang !== 'en') { + this.setData({ outputText: '翻译中 (2/3)..' }); + this.translateViaHF(text, sourceLang, 'en', (englishResult) => { + if (englishResult) { + // If the final target was English, we are done + if (targetLang === 'en') { + this.setData({ outputText: englishResult }); + } else { + this.setData({ outputText: '翻译中 (3/3)..' }); + this.translateViaHF(englishResult, 'en', targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + } + }); + } else { + // If source is English, directly translate to target + this.setData({ outputText: '翻译中 (2/2)..' }); + this.translateViaHF(text, sourceLang, targetLang, (finalResult) => { + if (finalResult) { + this.setData({ outputText: finalResult }); + } + }); + } + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 30000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { + callback(res.data.translated_text); + } else { + this.setData({ outputText: `HF翻译失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }, + fail: () => { + this.setData({ outputText: `HF翻译请求失败 (${sourceLang}->${targetLang})` }); + callback(null); + } + }); + } +}); \ No newline at end of file diff --git a/pages/index/index_v9_old.js b/pages/index/index_v9_old.js new file mode 100644 index 0000000000000000000000000000000000000000..e747cf54d3148a92e7834a8cc3e3df94df586496 --- /dev/null +++ b/pages/index/index_v9_old.js @@ -0,0 +1,75 @@ +const plugin = requirePlugin('WechatSI'); +const manager = plugin.getRecordRecognitionManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN' }, + 'en': { name: 'English', flag: 'us', code: 'en_US' } + }, + langCodes: ['zh', 'en'], // Only CN and EN for this test version + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + sourceLanguages: [], + targetLanguages: [] + }, + + onLoad: function () { + this.initializeLanguages(); + this.initRecorderManager(); + }, + + // --- Language Selection Logic (Simplified for CN/EN only) --- + initializeLanguages: function () { + const { langCodes, languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === sourceLang })), + targetLanguages: langCodes.map(c => ({ ...languages[c], langCode: c, selected: c === targetLang })) + }); + }, + selectSourceLanguage: function(e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function(e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function() { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Recorder Manager Initialization (Pure Plugin Mode) --- + initRecorderManager: function() { + manager.onStart = () => { + this.setData({ transcript: '正在聆听...', outputText: '' }); + }; + + // Only onStop is used for final results + manager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.result) { + this.setData({ transcript: res.result, outputText: res.translateResult || '' }); + } else { + this.setData({ transcript: '识别结果为空', outputText: '' }); + } + }; + + manager.onError = (res) => { + this.setData({ isRecording: false, transcript: '识别失败', outputText: res.msg }); + }; + }, + + // --- Recording Start/Stop (Pure Plugin Mode) --- + startRecording: function () { + const { sourceLang, targetLang } = this.data; + this.setData({ isRecording: true }); + manager.start({ lang: this.data.languages[sourceLang].code, translate: true, lto: this.data.languages[targetLang].code }); + }, + + stopRecording: function () { + manager.stop(); + } +}); \ No newline at end of file diff --git a/pages/index/indexz_v23_old.js b/pages/index/indexz_v23_old.js new file mode 100644 index 0000000000000000000000000000000000000000..f0df0dad6b72c577c6caeb87c1453ed801a4ac0d --- /dev/null +++ b/pages/index/indexz_v23_old.js @@ -0,0 +1,180 @@ +const plugin = requirePlugin("WechatSI"); +const pluginManager = plugin.getRecordRecognitionManager(); +const nativeRecorderManager = wx.getRecorderManager(); + +Page({ + data: { + languages: { + 'zh': { name: '中文', flag: 'cn', code: 'zh_CN', usePlugin: true }, + 'en': { name: 'English', flag: 'us', code: 'en_US', usePlugin: true }, + 'ja': { name: '日本語', flag: 'jp', code: 'ja_JP', usePlugin: false }, + 'ko': { name: '한국어', flag: 'kr', code: 'ko_KR', usePlugin: false } + }, + langCodes: ['zh', 'en', 'ja', 'ko'], + sourceLang: 'zh', + targetLang: 'en', + transcript: '', + outputText: '', + isRecording: false, + hfSpaceUrl: 'https://dazaozi-wechat-translator-app.hf.space', + currentManagerType: null // 'plugin' or 'native' + }, + + onLoad: function () { + this.initializeLanguages(); + this.initPluginManager(); + this.initNativeRecorderManager(); + }, + + // --- Language Selection Logic (Simplified) --- + initializeLanguages: function () { + const { languages, sourceLang, targetLang } = this.data; + this.setData({ + sourceLanguages: Object.keys(languages).map(key => ({ ...languages[key], langCode: key, selected: key === sourceLang })), + targetLanguages: Object.keys(languages).map(key => ({ ...languages[key], langCode: key, selected: key === targetLang })) + }); + }, + selectSourceLanguage: function (e) { this.setData({ sourceLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + selectTargetLanguage: function (e) { this.setData({ targetLang: e.currentTarget.dataset.langCode }, this.initializeLanguages); }, + swapLanguages: function () { + this.setData({ + sourceLang: this.data.targetLang, + targetLang: this.data.sourceLang, + transcript: this.data.outputText, + outputText: this.data.transcript + }, this.initializeLanguages); + }, + + // --- Manager Initializations with Detailed Error Logging --- + initPluginManager: function () { + pluginManager.onStart = () => this.setData({ transcript: '正在聆听 (插件)...', outputText: '' }); + pluginManager.onRecognize = (res) => this.setData({ transcript: res.result }); + pluginManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (!res.result) { return this.setData({ transcript: '识别结果为空' }); } + this.setData({ transcript: res.result }); + const { sourceLang, targetLang } = this.data; + const isPluginTarget = this.data.languages[targetLang].usePlugin; + if (isPluginTarget) { + this.translateViaPlugin(res.result); + } else { + this.translateViaHfBridge(res.result, sourceLang, targetLang); + } + }; + pluginManager.onError = (res) => this.setData({ isRecording: false, transcript: `插件错误: ${res.msg}` }); + }, + + initNativeRecorderManager: function () { + nativeRecorderManager.onStart = () => this.setData({ transcript: '正在聆听 (原生)...', outputText: '' }); + nativeRecorderManager.onStop = (res) => { + this.setData({ isRecording: false }); + if (res.tempFilePath) { this.uploadAudioForASR(res.tempFilePath); } + else { this.setData({ transcript: '原生录音文件获取失败' }); } + }; + nativeRecorderManager.onError = (res) => this.setData({ isRecording: false, transcript: `原生录音失败: ${res.errMsg}` }); + }, + + // --- Main Record Button Handler --- + handleRecordToggle: function() { + if (this.data.isRecording) { + this.stopRecording(); + return; + } + // Check permissions before starting a new recording + wx.getSetting({ + success: (res) => { + if (!res.authSetting['scope.record']) { + wx.authorize({ + scope: 'scope.record', + success: this.startRecording, + fail: () => wx.showToast({ title: '您拒绝了录音权限', icon: 'none' }) + }); + } else { + this.startRecording(); + } + }, + fail: () => wx.showToast({ title: '无法获取权限设置', icon: 'none' }) + }); + }, + + startRecording: function () { + const { sourceLang, languages } = this.data; + const shouldUsePlugin = languages[sourceLang].usePlugin; + const managerType = shouldUsePlugin ? 'plugin' : 'native'; + + this.setData({ isRecording: true, currentManagerType: managerType }); + + if (shouldUsePlugin) { + pluginManager.start({ lang: languages[sourceLang].code, translate: false }); + } else { + nativeRecorderManager.start({ format: 'mp3', sampleRate: 16000, numberOfChannels: 1 }); + } + }, + + stopRecording: function () { + if (this.data.currentManagerType === 'native') { + nativeRecorderManager.stop(); + } else { + pluginManager.stop(); + } + }, + + // --- Translation Logic --- + translateViaPlugin: function(text) { + const { sourceLang, targetLang, languages } = this.data; + if (sourceLang === targetLang) { return this.setData({ outputText: text }); } + this.setData({ outputText: '翻译中 (插件)...' }); + plugin.translate({ + lfrom: languages[sourceLang].code, + lto: languages[targetLang].code, + content: text, + success: (res) => { + if (res.retcode === 0) { this.setData({ outputText: res.result }); } + else { this.setData({ outputText: `插件翻译失败: ${res.retcode}` }); } + }, + fail: (err) => this.setData({ outputText: '插件翻译接口调用失败' }) + }); + }, + + uploadAudioForASR: function (filePath) { + this.setData({ transcript: '正在识别 (HF)...' }); + wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: (res) => { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/asr`, + method: 'POST', + data: { "audio_data_uri": `data:audio/mp3;base64,${res.data}` }, + timeout: 60000, + success: (asrRes) => { + if (asrRes.statusCode === 200 && asrRes.data.transcript) { + const transcript = asrRes.data.transcript; + this.setData({ transcript }); + this.translateViaHfBridge(transcript, this.data.sourceLang, this.data.targetLang); + } else { this.setData({ transcript: 'HF识别失败', outputText: asrRes.data.error || '' }); } + }, + fail: () => this.setData({ transcript: 'HF识别请求失败' }) + }); + }}); + }, + + translateViaHfBridge: function(text, source, target) { + if (source === target) { return this.setData({ outputText: text }); } + this.setData({ outputText: '翻译中 (HF)...' }); + this.translateViaHF(text, source, target, (result) => { + if (result) { this.setData({ outputText: result }); } + }); + }, + + translateViaHF: function(text, sourceLang, targetLang, callback) { + wx.request({ + url: `${this.data.hfSpaceUrl}/api/translate`, + method: 'POST', + data: { "text": text, "source_lang": sourceLang, "target_lang": targetLang }, + timeout: 45000, + success: (res) => { + if (res.statusCode === 200 && res.data.translated_text) { callback(res.data.translated_text); } + else { this.setData({ outputText: `HF翻译失败: ${res.data.error || '未知错误'}` }); callback(null); } + }, + fail: () => { this.setData({ outputText: 'HF翻译请求失败' }); callback(null); } + }); + } +}); diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000000000000000000000000000000000000..39a1fabbc9781f172488811ee76ce89903db1c00 --- /dev/null +++ b/project.config.json @@ -0,0 +1,68 @@ +{ + "miniprogramRoot": "./", + "projectname": "wechat-miniprogram-translator", + "description": "实时翻译工具微信小程序版", + "appid": "wx4bf4c3f1ca6b34b0", + "setting": { + "urlCheck": true, + "es6": true, + "enhance": true, + "postcss": true, + "minified": true, + "newFeature": true, + "coverView": true, + "nodeModules": true, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "scopeDataCheck": false, + "checkSiteMap": true, + "enableEngineNative": false, + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "condition": { + "search": { + "list": [] + }, + "conversation": { + "list": [] + }, + "plugin": { + "list": [] + }, + "sitemap": { + "list": [] + }, + "enableTools": true + }, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "disableUseStrict": false, + "useCompilerPlugins": false, + "swc": false, + "disableSWC": true + }, + "compileType": "miniprogram", + "libVersion": "3.8.10", + "srcMiniprogramRoot": "", + "editorSetting": { + "tabIndent": "tab", + "tabSize": 4 + }, + "simulatorPluginLibVersion": {}, + "packOptions": { + "ignore": [], + "include": [] + } +} \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000000000000000000000000000000000000..d450876288d0bdec94d9ba7b20bc7cc4b03bbdbb --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,22 @@ +{ + "libVersion": "3.8.10", + "projectname": "%E4%BD%A0%E8%AF%B4%E6%88%91%E8%AF%91project-hf", + "setting": { + "urlCheck": false, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "compileHotReLoad": true, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true, + "bigPackageSizeSupport": false + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3c0bc0d2ed3e498d4bc4327eb56a4c555961baac..32af0ba88f095383654e88f7c228d6f2b50d5059 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,10 @@ transformers torch sentencepiece -gradio soundfile sacremoses fastapi uvicorn deepl python-dotenv -optimum[onnxruntime] +optimum[onnxruntime] \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..f735f3a6bf11ac2703b1dc4898429d93295424a3 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,917 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^5.1.0", + "node-fetch": "^3.3.2" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000000000000000000000000000000000000..36c66c52b807d1022401d78295cdf8dcd495d3a1 --- /dev/null +++ b/server/package.json @@ -0,0 +1,19 @@ +{ + "name": "server", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "express": "^4.19.2", + "fluent-ffmpeg": "^2.1.2", + "form-data": "^4.0.0", + "multer": "^1.4.5-lts.1", + "node-fetch": "^2.7.0" + } +} diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000000000000000000000000000000000000..c2bc69ff386633e3114162ec0e9cd0541dbae5d0 --- /dev/null +++ b/server/server.js @@ -0,0 +1,97 @@ +const express = require('express'); +const multer = require('multer'); +const FormData = require('form-data'); +const axios = require('axios'); // 使用 axios 替换 node-fetch +const fetch = require('node-fetch'); // 保留用于翻译接口 + +const app = express(); +const port = 3000; + +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + next(); +}); + +const PYTHON_ASR_SERVICE_URL = 'http://localhost:5001/transcribe'; + +// ASR Endpoint: 使用 axios 重写,确保超时正确生效 +app.post('/asr', upload.single('audio'), async (req, res) => { + console.log("Received ASR request, proxying to Python service with axios..."); + + if (!req.file) { + return res.status(400).json({ error: 'No audio file uploaded.' }); + } + + try { + const form = new FormData(); + form.append('audio', req.file.buffer, { + filename: req.file.originalname, + contentType: req.file.mimetype, + }); + + const langCode = req.body.sourceLang.split('-')[0]; + form.append('language', langCode); + console.log(`Forwarding audio buffer with language '${langCode}'...`); + + // 使用 axios 发送请求,并设置一个可靠的60秒超时 + const asrResponse = await axios.post(PYTHON_ASR_SERVICE_URL, form, { + headers: form.getHeaders(), + timeout: 60000 // 60秒超时,作用于整个请求 + }); + + const asrData = asrResponse.data; + + console.log(`Transcription result: "${asrData.transcript}"`); + res.json({ transcript: asrData.transcript }); + + } catch (error) { + if (error.code === 'ECONNABORTED') { + console.error("Failed to process ASR request: Axios request timed out."); + res.status(504).json({ error: 'Request to ASR service timed out.' }); + } else { + console.error("Failed to process ASR request:", error.message); + res.status(500).json({ error: error.message }); + } + } +}); + +// Translate Endpoint (保持不变) +app.post('/translate', async (req, res) => { + const { text, sourceLang, targetLang } = req.body; + if (!text || !sourceLang || !targetLang) { + return res.status(400).json({ error: 'Missing text, sourceLang, or targetLang' }); + } + const source = sourceLang.split('-')[0]; + const target = targetLang.split('-')[0]; + if (source === target) { + return res.json({ translatedText: text }); + } + const apiUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`; + try { + const response = await fetch(apiUrl); + const data = await response.json(); + if (data && data[0] && data[0][0] && data[0][0][0]) { + const translatedText = data[0].map(segment => segment[0]).join(''); + res.json({ translatedText }); + } else { + throw new Error('Unexpected API response from translation service'); + } + } catch (error) { + console.error('Translation request error:', error.message); + res.status(500).json({ error: 'Translation failed' }); + } +}); + +app.get('/', (req, res) => { + res.status(200).send('Node.js server is running and healthy!'); +}); + +app.listen(port, () => { + console.log(`Node.js proxy server listening at http://localhost:${port}`); +}); diff --git a/server/server.js.old b/server/server.js.old new file mode 100644 index 0000000000000000000000000000000000000000..0ccf553fa61d0b226e1745067e3f49087f5731a6 --- /dev/null +++ b/server/server.js.old @@ -0,0 +1,64 @@ +// wechat-miniprogram-translator/server/server.js +const express = require('express'); +const fetch = require('node-fetch'); +const app = express(); +const port = 3000; + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// CORS for development (allow requests from any origin) +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + next(); +}); + +// ASR (Speech-to-Text) Endpoint - Placeholder +app.post('/asr', (req, res) => { + // In a real application, you would receive the audio file here (e.g., via multipart/form-data) + // and send it to a real ASR service (e.g., Tencent Cloud, Baidu AI, Google Speech-to-Text). + // For this prototype, we'll simulate a response. + console.log('Received ASR request. Simulating response...'); + const simulatedText = '这是模拟的语音识别结果。'; // You can change this for testing + res.json({ transcript: simulatedText }); +}); + +// Translation Endpoint - Proxy for Google Translate API +app.post('/translate', async (req, res) => { + const { text, sourceLang, targetLang } = req.body; + + if (!text || !sourceLang || !targetLang) { + return res.status(400).json({ error: 'Missing text, sourceLang, or targetLang' }); + } + + const source = sourceLang.split('-')[0]; + const target = targetLang.split('-')[0]; + + if (source === target) { + return res.json({ translatedText: text }); + } + + const apiUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`; + + try { + const response = await fetch(apiUrl); + const data = await response.json(); + + if (data && data[0] && data[0][0] && data[0][0][0]) { + const translatedText = data[0].map(segment => segment[0]).join(''); + res.json({ translatedText }); + } else { + console.error('Translation API returned unexpected data:', data); + res.status(500).json({ error: 'Translation failed: Unexpected API response' }); + } + } catch (error) { + console.error('Error calling Google Translate API:', error); + res.status(500).json({ error: 'Translation failed: Network error or API issue' }); + } +}); + +app.listen(port, () => { + console.log(`Backend server listening at http://localhost:${port}`); + console.log("Remember to start this server before running the Mini Program!"); +}); diff --git a/start_asr.sh b/start_asr.sh new file mode 100644 index 0000000000000000000000000000000000000000..e3f96c3610353dc4e1101d23401e2e2d504d7220 --- /dev/null +++ b/start_asr.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# This script starts the Gunicorn server for the ASR application. + +echo "Starting ASR service with Gunicorn..." + +# Absolute path to the project directory +PROJECT_DIR="/home/ubuntu/wechat-miniprogram-translator" + +# Absolute path to the Gunicorn executable within the venv +VENV_GUNICORN="${PROJECT_DIR}/venv/bin/gunicorn" + +# Check if the Gunicorn executable exists +if [ ! -f "$VENV_GUNICORN" ]; then + echo "Error: Gunicorn executable not found at $VENV_GUNICORN" + echo "Please make sure you have run 'pip install gunicorn' in your venv." + exit 1 +fi + +# Navigate to the directory containing asr_server.py +cd "$PROJECT_DIR" || exit + +# Explanation of parameters: +# --workers 1: Use a single worker process. +# --max-requests 1: CRITICAL! Restart the worker after every single request to prevent state issues. +# --timeout 120: Allow each worker up to 120 seconds to process a request. +# --bind 0.0.0.0:5001: Listen on port 5001 on all network interfaces. +# asr_server:app: The Flask application instance 'app' from the 'asr_server.py' file. +# "$@": This is the crucial part. It takes all arguments passed to this script +# (from ecosystem.config.js, e.g., --model tiny) and passes them to the python script. +exec "$VENV_GUNICORN" --workers 1 --max-requests 1 --timeout 120 --bind 0.0.0.0:5001 asr_server:app -- "$@" \ No newline at end of file diff --git a/translate.py b/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..66d3030de2a4566361a6876b8fa220da00692f23 --- /dev/null +++ b/translate.py @@ -0,0 +1,51 @@ +import sys +from transformers import MarianMTModel, MarianTokenizer + +def translate_text(text, source_lang, target_lang): + """ + Translates text from a source language to a target language. + """ + try: + # Define the model name based on source and target languages + # The format is 'Helsinki-NLP/opus-mt-{source}-{target}' + model_name = f'Helsinki-NLP/opus-mt-{source_lang}-{target_lang}' + + # Load the tokenizer and model. + # The first time a model is used, it will be downloaded from Hugging Face. + # This might take a moment. Subsequent uses will load from cache. + tokenizer = MarianTokenizer.from_pretrained(model_name) + model = MarianMTModel.from_pretrained(model_name) + + # Tokenize the input text + tokenized_text = tokenizer(text, return_tensors="pt", padding=True) + + # Generate the translation + translated_tokens = model.generate(**tokenized_text) + + # Decode the translated tokens into text + translated_text = tokenizer.decode(translated_tokens[0], skip_special_tokens=True) + + return translated_text + except Exception as e: + # Handle cases where a direct model doesn't exist (e.g., zh-es) + # or other errors. + return f"Error during translation: {str(e)}" + + +if __name__ == "__main__": + # The script expects three arguments: text, source_lang, target_lang + if len(sys.argv) != 4: + print("Usage: python translate.py ") + sys.exit(1) + + input_text = sys.argv[1] + source_language = sys.argv[2] + target_language = sys.argv[3] + + # The models use 2-letter language codes (e.g., 'en', 'zh', 'es') + # We take the first part of the lang code (e.g., 'zh-CN' -> 'zh') + source_code = source_language.split('-')[0] + target_code = target_language.split('-')[0] + + translated_output = translate_text(input_text, source_code, target_code) + print(translated_output) diff --git a/wechat-miniprogram-translator-hf/app.py b/wechat-miniprogram-translator-hf/app.py new file mode 100644 index 0000000000000000000000000000000000000000..605ae1c1c10cfdf0745e6a7c177c0e261ed11d2d --- /dev/null +++ b/wechat-miniprogram-translator-hf/app.py @@ -0,0 +1,85 @@ +import gradio as gr +from transformers import pipeline +import torch + +# 1. Load Models (this will happen only once when the app starts) +print("Loading models...") + +# ASR Pipeline +asr_pipeline = pipeline( + "automatic-speech-recognition", + model="openai/whisper-small", + torch_dtype=torch.float16, # Use float16 for faster inference + device="cpu" # Specify CPU device +) + +# Translation Pipelines +translators = { + "en-zh": pipeline("translation", model="Helsinki-NLP/opus-mt-en-zh"), + "zh-en": pipeline("translation", model="Helsinki-NLP/opus-mt-zh-en"), + "en-ja": pipeline("translation", model="Helsinki-NLP/opus-mt-en-ja"), + "ja-en": pipeline("translation", model="Helsinki-NLP/opus-mt-ja-en"), + "en-ko": pipeline("translation", model="Helsinki-NLP/opus-mt-en-ko"), + "ko-en": pipeline("translation", model="Helsinki-NLP/opus-mt-ko-en"), +} + +print("Models loaded successfully.") + +# 2. Define Processing Functions + +def transcribe_audio(audio_file): + print(f"Received audio file: {audio_file}") + if audio_file is None: + return "" + try: + # The pipeline expects a file path + text = asr_pipeline(audio_file)["text"] + print(f"ASR result: {text}") + return text + except Exception as e: + print(f"Error in ASR: {e}") + return f"Error in ASR: {e}" + +def translate_text(text, source_lang, target_lang): + print(f"Translating '{text}' from {source_lang} to {target_lang}") + if not text: + return "" + + # Direct translation if possible + if f"{source_lang}-{target_lang}" in translators: + translator = translators[f"{source_lang}-{target_lang}"] + translated_text = translator(text)[0]['translation_text'] + # Bridge translation via English + elif source_lang != 'en' and target_lang != 'en': + to_english_translator = translators[f"{source_lang}-en"] + english_text = to_english_translator(text)[0]['translation_text'] + + from_english_translator = translators[f"en-{target_lang}"] + translated_text = from_english_translator(english_text)[0]['translation_text'] + else: + return "Translation route not supported" + + print(f"Translation result: {translated_text}") + return translated_text + +# 3. Create Gradio Interface +with gr.Blocks() as demo: + gr.Markdown("## All-in-One ASR and Translation API") + + with gr.Tab("ASR"): + audio_input = gr.Audio(type="filepath", label="Upload Audio") + asr_output = gr.Textbox(label="Transcript") + asr_button = gr.Button("Transcribe") + asr_button.click(transcribe_audio, inputs=audio_input, outputs=asr_output, api_name="asr") + + with gr.Tab("Translate"): + text_input = gr.Textbox(label="Input Text") + source_lang_input = gr.Dropdown(["en", "zh", "ja", "ko"], label="Source Language") + target_lang_input = gr.Dropdown(["en", "zh", "ja", "ko"], label="Target Language") + translation_output = gr.Textbox(label="Translation") + translate_button = gr.Button("Translate") + translate_button.click(translate_text, inputs=[text_input, source_lang_input, target_lang_input], outputs=translation_output, api_name="translate") + +# 4. Launch the App +if __name__ == "__main__": + demo.launch() diff --git a/wechat-miniprogram-translator-hf/requirements.txt b/wechat-miniprogram-translator-hf/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..be8cca5048010af9263787905113eb182f946b6f --- /dev/null +++ b/wechat-miniprogram-translator-hf/requirements.txt @@ -0,0 +1,4 @@ +transformers +torch +sentencepiece +gradio