修复网速慢,语音播放问题
Browse files- youtube_sub.js +91 -31
youtube_sub.js
CHANGED
|
@@ -27,6 +27,8 @@ GM_addStyle(`
|
|
| 27 |
if (window.subtitleManagerInstance) {
|
| 28 |
window.subtitleManagerInstance.cleanup();
|
| 29 |
}
|
|
|
|
|
|
|
| 30 |
|
| 31 |
// 增强的样式定义,添加过渡效果
|
| 32 |
const styles = `
|
|
@@ -901,6 +903,69 @@ GM_addStyle(`
|
|
| 901 |
this.container.classList.toggle('collapsed', this.isCollapsed);
|
| 902 |
}
|
| 903 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
// Update displaySingleLanguage method
|
| 905 |
displaySingleLanguage(container, subtitles) {
|
| 906 |
subtitles.forEach((sub, index) => {
|
|
@@ -969,7 +1034,7 @@ GM_addStyle(`
|
|
| 969 |
speakerBtn = document.createElement('button');
|
| 970 |
speakerBtn.className = 'speaker-btn';
|
| 971 |
speakerBtn.textContent = '🔊';
|
| 972 |
-
speakerBtn.onclick = (e) => {
|
| 973 |
e.stopPropagation();
|
| 974 |
try {
|
| 975 |
const utterance = new SpeechSynthesisUtterance(wordText);
|
|
@@ -977,7 +1042,22 @@ GM_addStyle(`
|
|
| 977 |
utterance.rate = 0.9;
|
| 978 |
|
| 979 |
// Get voices and select based on click count
|
| 980 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
const isFemaleTurn = this.speakerClickCount % 2 === 1;
|
| 982 |
const voice = voices.find(v =>
|
| 983 |
v.lang.includes('en') &&
|
|
@@ -985,11 +1065,18 @@ GM_addStyle(`
|
|
| 985 |
);
|
| 986 |
|
| 987 |
if (voice) {
|
|
|
|
| 988 |
utterance.voice = voice;
|
|
|
|
|
|
|
| 989 |
}
|
| 990 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 991 |
speechSynthesis.speak(utterance);
|
| 992 |
-
this.speakerClickCount++;
|
| 993 |
} catch (error) {
|
| 994 |
console.error('TTS failed:', error);
|
| 995 |
}
|
|
@@ -1044,34 +1131,7 @@ GM_addStyle(`
|
|
| 1044 |
// });
|
| 1045 |
// });
|
| 1046 |
|
| 1047 |
-
const response = await
|
| 1048 |
-
GM_xmlhttpRequest({
|
| 1049 |
-
method: 'GET',
|
| 1050 |
-
url: `https://sonygod-flash.hf.space/translate/${encodeURIComponent(word.text.trim())}`,
|
| 1051 |
-
headers: {
|
| 1052 |
-
'Accept': 'application/json',
|
| 1053 |
-
'Origin': 'https://www.youtube.com'
|
| 1054 |
-
},
|
| 1055 |
-
onload: function (response) {
|
| 1056 |
-
if (response.status >= 200 && response.status < 300) {
|
| 1057 |
-
try {
|
| 1058 |
-
resolve(JSON.parse(response.responseText));
|
| 1059 |
-
} catch (e) {
|
| 1060 |
-
reject(new Error('Invalid JSON response'));
|
| 1061 |
-
}
|
| 1062 |
-
} else {
|
| 1063 |
-
reject(new Error(`HTTP error! status: ${response.status}`));
|
| 1064 |
-
}
|
| 1065 |
-
},
|
| 1066 |
-
onerror: function (error) {
|
| 1067 |
-
reject(new Error('Network request failed: ' + error.error));
|
| 1068 |
-
},
|
| 1069 |
-
ontimeout: function () {
|
| 1070 |
-
reject(new Error('Request timed out'));
|
| 1071 |
-
}
|
| 1072 |
-
});
|
| 1073 |
-
});
|
| 1074 |
-
|
| 1075 |
console.log('API Response:', response);
|
| 1076 |
|
| 1077 |
const content = response?.data?.response;
|
|
|
|
| 27 |
if (window.subtitleManagerInstance) {
|
| 28 |
window.subtitleManagerInstance.cleanup();
|
| 29 |
}
|
| 30 |
+
const WORD_CACHE_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
|
| 31 |
+
const WORD_CACHE_KEY = 'youtube_subtitle_word_cache';
|
| 32 |
|
| 33 |
// 增强的样式定义,添加过渡效果
|
| 34 |
const styles = `
|
|
|
|
| 903 |
this.container.classList.toggle('collapsed', this.isCollapsed);
|
| 904 |
}
|
| 905 |
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
// Get word translation with cache
|
| 909 |
+
async getWordTranslation(word) {
|
| 910 |
+
// Get cache
|
| 911 |
+
const cache = JSON.parse(localStorage.getItem(WORD_CACHE_KEY) || '{}');
|
| 912 |
+
const now = Date.now();
|
| 913 |
+
const cachedResult = cache[word];
|
| 914 |
+
|
| 915 |
+
// Check valid cache
|
| 916 |
+
if (cachedResult && now - cachedResult.timestamp < WORD_CACHE_EXPIRY) {
|
| 917 |
+
console.log('Cache hit:', word);
|
| 918 |
+
return cachedResult.data;
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
// Clear expired entries
|
| 922 |
+
Object.keys(cache).forEach(key => {
|
| 923 |
+
if (now - cache[key].timestamp > WORD_CACHE_EXPIRY) {
|
| 924 |
+
delete cache[key];
|
| 925 |
+
}
|
| 926 |
+
});
|
| 927 |
+
|
| 928 |
+
// API call
|
| 929 |
+
const response = await new Promise((resolve, reject) => {
|
| 930 |
+
GM_xmlhttpRequest({
|
| 931 |
+
method: 'GET',
|
| 932 |
+
url: `https://sonygod-flash.hf.space/translate/${encodeURIComponent(word.trim())}`,
|
| 933 |
+
headers: {
|
| 934 |
+
'Accept': 'application/json',
|
| 935 |
+
'Origin': 'https://www.youtube.com'
|
| 936 |
+
},
|
| 937 |
+
onload: function (response) {
|
| 938 |
+
if (response.status >= 200 && response.status < 300) {
|
| 939 |
+
try {
|
| 940 |
+
resolve(JSON.parse(response.responseText));
|
| 941 |
+
} catch (e) {
|
| 942 |
+
reject(new Error('Invalid JSON response'));
|
| 943 |
+
}
|
| 944 |
+
} else {
|
| 945 |
+
reject(new Error(`HTTP error! status: ${response.status}`));
|
| 946 |
+
}
|
| 947 |
+
},
|
| 948 |
+
onerror: error => reject(new Error('Network request failed: ' + error.error)),
|
| 949 |
+
ontimeout: () => reject(new Error('Request timed out'))
|
| 950 |
+
});
|
| 951 |
+
});
|
| 952 |
+
|
| 953 |
+
// Cache successful response
|
| 954 |
+
if (response?.data?.response) {
|
| 955 |
+
cache[word] = {
|
| 956 |
+
timestamp: now,
|
| 957 |
+
data: response
|
| 958 |
+
};
|
| 959 |
+
try {
|
| 960 |
+
localStorage.setItem(WORD_CACHE_KEY, JSON.stringify(cache));
|
| 961 |
+
} catch (e) {
|
| 962 |
+
console.error('Cache write failed:', e);
|
| 963 |
+
}
|
| 964 |
+
}
|
| 965 |
+
|
| 966 |
+
return response;
|
| 967 |
+
};
|
| 968 |
+
|
| 969 |
// Update displaySingleLanguage method
|
| 970 |
displaySingleLanguage(container, subtitles) {
|
| 971 |
subtitles.forEach((sub, index) => {
|
|
|
|
| 1034 |
speakerBtn = document.createElement('button');
|
| 1035 |
speakerBtn.className = 'speaker-btn';
|
| 1036 |
speakerBtn.textContent = '🔊';
|
| 1037 |
+
speakerBtn.onclick = async (e) => {
|
| 1038 |
e.stopPropagation();
|
| 1039 |
try {
|
| 1040 |
const utterance = new SpeechSynthesisUtterance(wordText);
|
|
|
|
| 1042 |
utterance.rate = 0.9;
|
| 1043 |
|
| 1044 |
// Get voices and select based on click count
|
| 1045 |
+
let voices = speechSynthesis.getVoices();
|
| 1046 |
+
if (!voices.length) {
|
| 1047 |
+
await new Promise(resolve => {
|
| 1048 |
+
speechSynthesis.onvoiceschanged = () => {
|
| 1049 |
+
voices = speechSynthesis.getVoices();
|
| 1050 |
+
resolve();
|
| 1051 |
+
};
|
| 1052 |
+
});
|
| 1053 |
+
}
|
| 1054 |
+
// Log available voices
|
| 1055 |
+
console.log('Available voices:', voices.map(v => ({
|
| 1056 |
+
name: v.name,
|
| 1057 |
+
lang: v.lang,
|
| 1058 |
+
isFemale: v.name.toLowerCase().includes('female')
|
| 1059 |
+
})));
|
| 1060 |
+
// Select voice based on click count
|
| 1061 |
const isFemaleTurn = this.speakerClickCount % 2 === 1;
|
| 1062 |
const voice = voices.find(v =>
|
| 1063 |
v.lang.includes('en') &&
|
|
|
|
| 1065 |
);
|
| 1066 |
|
| 1067 |
if (voice) {
|
| 1068 |
+
console.log('Selected voice:', voice.name);
|
| 1069 |
utterance.voice = voice;
|
| 1070 |
+
} else {
|
| 1071 |
+
console.warn('No matching voice found, using default');
|
| 1072 |
}
|
| 1073 |
|
| 1074 |
+
// Cancel any ongoing speech
|
| 1075 |
+
speechSynthesis.cancel();
|
| 1076 |
+
|
| 1077 |
+
// Speak the word
|
| 1078 |
speechSynthesis.speak(utterance);
|
| 1079 |
+
this.speakerClickCount++;
|
| 1080 |
} catch (error) {
|
| 1081 |
console.error('TTS failed:', error);
|
| 1082 |
}
|
|
|
|
| 1131 |
// });
|
| 1132 |
// });
|
| 1133 |
|
| 1134 |
+
const response = await this.getWordTranslation(wordText);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
console.log('API Response:', response);
|
| 1136 |
|
| 1137 |
const content = response?.data?.response;
|