diff --git a/src/config/setting.js b/src/config/setting.js
index 6a39e7d..477700b 100644
--- a/src/config/setting.js
+++ b/src/config/setting.js
@@ -95,33 +95,18 @@ export const DEFAULT_TRANBOX_SETTING = {
enSug: OPT_SUG_YOUDAO, // 英文建议
};
-const SUBTITLE_WINDOW_STYLE = `container-type: inline-size;
-position: absolute;
-bottom: 10%;
-left: 50%;
-transform: translateX(-50%);
-width: 80%;
-padding: 10px;
-background-color: rgba(0, 0, 0, 0.7);
+const SUBTITLE_WINDOW_STYLE = `padding: 0.5em 1em;
+background-color: rgba(0, 0, 0, 0.5);
color: white;
-text-align: center;
line-height: 1.2;
text-shadow: 1px 1px 2px black;
-pointer-events: none;
-z-index: 2147483647;
opacity: 0;
-cursor: grab;
-transition: opacity 0.2s ease-in-out;`;
+transition: opacity 0.2s ease-in-out;
+display: inline-block`;
-const SUBTITLE_ORIGIN_STYLE = `margin:0;
-padding: 0;
-opacity: 0.8;
-font-size: clamp(1.5rem, 3cqw, 3rem);`;
+const SUBTITLE_ORIGIN_STYLE = `font-size: clamp(1.5rem, 3cqw, 3rem);`;
-const SUBTITLE_TRANSLATION_STYLE = `margin:0;
-padding: 0;
-opacity: 1;
-font-size: clamp(1.5rem, 3cqw, 3rem);`;
+const SUBTITLE_TRANSLATION_STYLE = `font-size: clamp(1.5rem, 3cqw, 3rem);`;
export const DEFAULT_SUBTITLE_SETTING = {
enabled: true, // 是否开启
diff --git a/src/libs/svg.js b/src/libs/svg.js
index 14ad5f5..34de861 100644
--- a/src/libs/svg.js
+++ b/src/libs/svg.js
@@ -21,7 +21,8 @@ export const loadingSvg = `
export const createLogoSvg = ({
width = "100%",
height = "100%",
- viewBox = "-13 -14 60 60",
+ viewBox = "-20 -20 70 70",
+ isSelected = false,
} = {}) => {
const svgNS = "http://www.w3.org/2000/svg";
const svgElement = document.createElementNS(svgNS, "svg");
@@ -51,5 +52,14 @@ export const createLogoSvg = ({
svgElement.appendChild(path1);
svgElement.appendChild(path2);
+ if (isSelected) {
+ const redLine = document.createElementNS(svgNS, "path");
+ redLine.setAttribute("d", "M0 36 L32 36");
+ redLine.setAttribute("stroke", "red");
+ redLine.setAttribute("stroke-width", "3");
+ redLine.setAttribute("stroke-linecap", "round");
+ svgElement.appendChild(redLine);
+ }
+
return svgElement;
};
diff --git a/src/libs/utils.js b/src/libs/utils.js
index 412682d..f436e15 100644
--- a/src/libs/utils.js
+++ b/src/libs/utils.js
@@ -362,3 +362,15 @@ export const truncateWords = (str, maxLength) => {
const truncated = str.slice(0, maxLength);
return truncated.slice(0, truncated.lastIndexOf(" ")) + " …";
};
+
+/**
+ * 生成随机数
+ * @param {*} min
+ * @param {*} max
+ * @param {*} integer
+ * @returns
+ */
+export const randomBetween = (min, max, integer = true) => {
+ const value = Math.random() * (max - min) + min;
+ return integer ? Math.floor(value) : value;
+};
diff --git a/src/subtitle/BilingualSubtitleManager.js b/src/subtitle/BilingualSubtitleManager.js
index a14b55b..f8a6ef2 100644
--- a/src/subtitle/BilingualSubtitleManager.js
+++ b/src/subtitle/BilingualSubtitleManager.js
@@ -51,7 +51,7 @@ export class BilingualSubtitleManager {
destroy() {
logger.info("Bilingual Subtitle Manager: Destroying...");
this.#removeEventListeners();
- this.#captionWindowEl?.parentElement?.remove();
+ this.#captionWindowEl?.parentElement?.parentElement?.remove();
this.#formattedSubtitles = [];
}
@@ -60,14 +60,36 @@ export class BilingualSubtitleManager {
*/
#createCaptionWindow() {
const container = document.createElement("div");
- container.className = `kiss-caption-window-container notranslate`;
- container.style.cssText = `position:absolute; width:100%; height:100%; left:0; top:0;`;
+ container.className = `kiss-caption-container notranslate`;
+ Object.assign(container.style, {
+ position: "absolute",
+ width: "100%",
+ height: "100%",
+ left: "0",
+ top: "0",
+ });
+
+ const paper = document.createElement("div");
+ paper.className = `kiss-caption-paper`;
+ Object.assign(paper.style, {
+ position: "absolute",
+ width: "80%",
+ left: "50%",
+ bottom: "10%",
+ transform: "translateX(-50%)",
+ textAlign: "center",
+ cursor: "grab",
+ containerType: "inline-size",
+ pointerEvents: "none",
+ zIndex: "2147483647",
+ });
this.#captionWindowEl = document.createElement("div");
this.#captionWindowEl.className = `kiss-caption-window`;
this.#captionWindowEl.style.cssText = this.#setting.windowStyle;
- container.appendChild(this.#captionWindowEl);
+ paper.appendChild(this.#captionWindowEl);
+ container.appendChild(paper);
const videoContainer = this.#videoEl.parentElement?.parentElement;
if (!videoContainer) {
diff --git a/src/subtitle/YouTubeCaptionProvider.js b/src/subtitle/YouTubeCaptionProvider.js
index fcef427..0d8d62c 100644
--- a/src/subtitle/YouTubeCaptionProvider.js
+++ b/src/subtitle/YouTubeCaptionProvider.js
@@ -2,9 +2,10 @@ import { logger } from "../libs/log.js";
import { apiTranslate } from "../apis/index.js";
import { BilingualSubtitleManager } from "./BilingualSubtitleManager.js";
import { getGlobalVariable } from "./globalVariable.js";
-import { MSG_XHR_DATA_YOUTUBE } from "../config";
-import { truncateWords } from "../libs/utils.js";
+import { MSG_XHR_DATA_YOUTUBE, APP_NAME } from "../config";
+import { truncateWords, sleep } from "../libs/utils.js";
import { createLogoSvg } from "../libs/svg.js";
+import { randomBetween } from "../libs/utils.js";
const VIDEO_SELECT = "#container video";
const CONTORLS_SELECT = ".ytp-right-controls";
@@ -15,6 +16,9 @@ class YouTubeCaptionProvider {
#videoId = "";
#subtitles = [];
#managerInstance = null;
+ #toggleButton = null;
+ #enabled = false;
+ #ytControls = null;
constructor(setting = {}) {
this.#setting = setting;
@@ -52,9 +56,21 @@ class YouTubeCaptionProvider {
});
}
+ async #doubleClick() {
+ const button = this.#ytControls.querySelector(
+ "button.ytp-subtitles-button"
+ );
+ if (button) {
+ await sleep(randomBetween(50, 100));
+ button.click();
+ await sleep(randomBetween(500, 1000));
+ button.click();
+ }
+ }
+
#injectToggleButton() {
- const controls = document.querySelector(CONTORLS_SELECT);
- if (!controls) {
+ this.#ytControls = document.querySelector(CONTORLS_SELECT);
+ if (!this.#ytControls) {
logger.warn("Youtube Provider: Could not find YouTube player controls.");
return;
}
@@ -68,30 +84,28 @@ class YouTubeCaptionProvider {
const toggleButton = document.createElement("button");
toggleButton.className =
"ytp-button notranslate kiss-bilingual-subtitle-button";
- toggleButton.title = "Toggle Bilingual Subtitles";
+ toggleButton.title = APP_NAME;
Object.assign(toggleButton.style, {
color: "white",
- opacity: "0.8",
+ opacity: "0.5",
});
toggleButton.appendChild(createLogoSvg());
kissControls.appendChild(toggleButton);
toggleButton.onclick = () => {
- if (!this.#managerInstance) {
+ if (!this.#enabled) {
logger.info(`Youtube Provider: Feature toggled ON.`);
- toggleButton.style.opacity = "1";
- this.#setting.enabled = true;
this.#startManager();
} else {
logger.info(`Youtube Provider: Feature toggled OFF.`);
- toggleButton.style.opacity = "0.5";
- this.#setting.enabled = false;
this.#destroyManager();
}
};
+ this.#toggleButton = toggleButton;
+ this.#ytControls.before(kissControls);
- controls.before(kissControls);
+ this.#doubleClick();
}
#findCaptionTrack(ytPlayer) {
@@ -144,6 +158,10 @@ class YouTubeCaptionProvider {
async #handleInterceptedRequest(url, responseText) {
try {
+ if (!responseText) {
+ return;
+ }
+
const ytPlayer = await getGlobalVariable("ytInitialPlayerResponse");
const captionTrack = this.#findCaptionTrack(ytPlayer);
if (!captionTrack) {
@@ -169,13 +187,13 @@ class YouTubeCaptionProvider {
responseText
);
if (!subtitleEvents) {
- logger.warn("Youtube Provider: SubtitleEvents not got.");
+ logger.info("Youtube Provider: SubtitleEvents not got.");
return;
}
const subtitles = this.#formatSubtitles(subtitleEvents);
if (subtitles.length === 0) {
- logger.warn("Youtube Provider: No subtitles after format.");
+ logger.info("Youtube Provider: No subtitles after format.");
return;
}
@@ -189,17 +207,22 @@ class YouTubeCaptionProvider {
this.#subtitles = subtitles;
this.#videoId = videoId;
- this.#destroyManager();
+ if (this.#toggleButton) {
+ this.#toggleButton.style.opacity = subtitles.length ? "1" : "0.5";
+ }
- if (this.#setting.enabled) {
+ if (this.#enabled) {
+ this.#destroyManager();
this.#startManager();
}
}
#startManager() {
- if (this.#managerInstance) {
+ if (this.#enabled || this.#managerInstance) {
return;
}
+ this.#enabled = true;
+ this.#toggleButton?.replaceChildren(createLogoSvg({ isSelected: true }));
const videoEl = document.querySelector(VIDEO_SELECT);
if (!videoEl) {
@@ -210,14 +233,12 @@ class YouTubeCaptionProvider {
if (this.#subtitles?.length === 0) {
// todo: 等待并给出用户提示
logger.info("Youtube Provider: No subtitles");
+ this.#doubleClick();
return;
}
logger.info("Youtube Provider: Starting manager...");
- const ytCaption = document.querySelector(YT_CAPTION_SELECT);
- ytCaption && (ytCaption.style.display = "none");
-
this.#managerInstance = new BilingualSubtitleManager({
videoEl,
formattedSubtitles: this.#subtitles,
@@ -225,21 +246,29 @@ class YouTubeCaptionProvider {
setting: this.#setting,
});
this.#managerInstance.start();
+
+ const ytCaption = document.querySelector(YT_CAPTION_SELECT);
+ ytCaption && (ytCaption.style.display = "none");
}
#destroyManager() {
+ if (!this.#enabled) {
+ return;
+ }
+ this.#enabled = false;
+ this.#toggleButton?.replaceChildren(createLogoSvg());
+
+ logger.info("Youtube Provider: Destroying manager...");
+
+ const ytCaption = document.querySelector(YT_CAPTION_SELECT);
+ ytCaption && (ytCaption.style.display = "block");
+
if (this.#managerInstance) {
- logger.info("Youtube Provider: Destroying manager...");
-
- const ytCaption = document.querySelector(YT_CAPTION_SELECT);
- ytCaption && (ytCaption.style.display = "block");
-
this.#managerInstance.destroy();
this.#managerInstance = null;
}
}
- // todo: 没有标点断句的处理
#formatSubtitles(data) {
const events = data?.events;
if (!Array.isArray(events)) return [];
@@ -300,10 +329,179 @@ class YouTubeCaptionProvider {
}
}
- return lines.map((line) => ({
- ...line,
- duration: Math.max(0, line.end - line.start),
- text: truncateWords(line.text.trim().replace(/\s+/g, " "), 300),
+ const isPoor = this.#isQualityPoor(lines);
+ if (isPoor) {
+ return this.#processSubtitles(data);
+ }
+
+ return lines.map((item) => ({
+ ...item,
+ duration: Math.max(0, item.end - item.start),
+ text: truncateWords(item.text.trim().replace(/\s+/g, " "), 250),
+ }));
+ }
+
+ #isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.1) {
+ if (lines.length === 0) return false;
+ const longLinesCount = lines.filter(
+ (line) => line.text.length > lengthThreshold
+ ).length;
+ return longLinesCount / lines.length > percentageThreshold;
+ }
+
+ #processSubtitles(data, { timeout = 1500, maxWords = 15 } = {}) {
+ const groupedPauseWords = {
+ 1: new Set([
+ "actually",
+ "also",
+ "although",
+ "and",
+ "anyway",
+ "as",
+ "basically",
+ "because",
+ "but",
+ "eventually",
+ "frankly",
+ "honestly",
+ "hopefully",
+ "however",
+ "if",
+ "instead",
+ "it's",
+ "just",
+ "let's",
+ "like",
+ "literally",
+ "maybe",
+ "meanwhile",
+ "nevertheless",
+ "nonetheless",
+ "now",
+ "okay",
+ "or",
+ "otherwise",
+ "perhaps",
+ "personally",
+ "probably",
+ "right",
+ "since",
+ "so",
+ "suddenly",
+ "that's",
+ "then",
+ "there's",
+ "therefore",
+ "though",
+ "thus",
+ "unless",
+ "until",
+ "well",
+ "while",
+ ]),
+ 2: new Set([
+ "after all",
+ "at first",
+ "at least",
+ "even if",
+ "even though",
+ "for example",
+ "for instance",
+ "i believe",
+ "i guess",
+ "i mean",
+ "i suppose",
+ "i think",
+ "in fact",
+ "in the end",
+ "of course",
+ "then again",
+ "to be fair",
+ "you know",
+ "you see",
+ ]),
+ 3: new Set([
+ "as a result",
+ "by the way",
+ "in other words",
+ "in that case",
+ "in this case",
+ "to be clear",
+ "to be honest",
+ ]),
+ };
+
+ const sentences = [];
+ let currentBuffer = [];
+ let bufferWordCount = 0;
+
+ const joinSegs = (segs) => ({
+ text: segs
+ .map((s) => s.text)
+ .join(" ")
+ .trim(),
+ start: segs[0].start,
+ end: segs[segs.length - 1].end,
+ });
+
+ const flushBuffer = () => {
+ if (currentBuffer.length > 0) {
+ sentences.push(joinSegs(currentBuffer));
+ }
+ currentBuffer = [];
+ bufferWordCount = 0;
+ };
+
+ data.events?.forEach((event) => {
+ event.segs?.forEach((seg, j) => {
+ const text = seg.utf8?.trim() || "";
+ if (!text) return;
+
+ const start = event.tStartMs + (seg.tOffsetMs ?? 0);
+ const lastSegment = currentBuffer[currentBuffer.length - 1];
+
+ if (lastSegment) {
+ if (!lastSegment.end) {
+ lastSegment.end = start;
+ }
+
+ const isEndOfSentence = /[.?!\]]$/.test(lastSegment.text);
+ const isTimeout = start - lastSegment.end > timeout;
+ const isWordLimitExceeded = bufferWordCount >= maxWords;
+ const startsWithPauseWord = groupedPauseWords["1"].has(
+ text.toLowerCase().split(" ")[0]
+ );
+
+ // todo: 考虑连词开头
+ const isNewClause =
+ (startsWithPauseWord && currentBuffer.length > 1) ||
+ text.startsWith("[");
+
+ if (
+ isEndOfSentence ||
+ isTimeout ||
+ isWordLimitExceeded ||
+ isNewClause
+ ) {
+ flushBuffer();
+ }
+ }
+
+ const currentSegment = { text, start };
+ if (j === event.segs.length - 1) {
+ currentSegment.end = event.tStartMs + event.dDurationMs;
+ }
+
+ currentBuffer.push(currentSegment);
+ bufferWordCount += text.split(/\s+/).length;
+ });
+ });
+
+ flushBuffer();
+
+ return sentences.map((item) => ({
+ ...item,
+ duration: item.end - item.start,
}));
}
}
diff --git a/src/subtitle/subtitle.js b/src/subtitle/subtitle.js
index c0d3408..192ae7c 100644
--- a/src/subtitle/subtitle.js
+++ b/src/subtitle/subtitle.js
@@ -12,6 +12,11 @@ const providers = [
export function runSubtitle({ href, setting, rule }) {
try {
+ const subtitleSetting = setting.subtitleSetting || DEFAULT_SUBTITLE_SETTING;
+ if (!subtitleSetting.enabled) {
+ return;
+ }
+
const provider = providers.find((item) => isMatch(href, item.pattern));
if (provider) {
const id = "kiss-translator-injector";
@@ -22,7 +27,7 @@ export function runSubtitle({ href, setting, rule }) {
setting.transApis.find((api) => api.apiSlug === rule.apiSlug) ||
DEFAULT_API_SETTING;
provider.start({
- ...(setting.subtitleSetting || DEFAULT_SUBTITLE_SETTING),
+ ...subtitleSetting,
apiSetting,
});
}
diff --git a/src/views/Options/Subtitle.js b/src/views/Options/Subtitle.js
index 1467f60..959b3e4 100644
--- a/src/views/Options/Subtitle.js
+++ b/src/views/Options/Subtitle.js
@@ -104,16 +104,6 @@ export default function SubtitleSetting() {
-
+
);