mirror of
https://github.com/imputnet/cobalt.git
synced 2026-10-25 03:22:19 +00:00
api/rutube: support SRT transcript sources
This commit is contained in:
parent
4b17927f90
commit
13431a1501
6 changed files with 66 additions and 10 deletions
|
|
@ -61,6 +61,30 @@ export const parseVtt = input => {
|
|||
return cues;
|
||||
};
|
||||
|
||||
export const parseSrt = input => {
|
||||
const normalized = input.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
||||
const cues = [];
|
||||
|
||||
for (const block of normalized.split(/\n{2,}/)) {
|
||||
const lines = block.split("\n").map(line => line.trimEnd());
|
||||
const timingIndex = lines.findIndex(line => line.includes(" --> "));
|
||||
if (timingIndex < 0) continue;
|
||||
|
||||
const timing = lines[timingIndex].match(
|
||||
/^(\d{2}:)?\d{2}:\d{2},\d{3}\s+-->\s+(\d{2}:)?\d{2}:\d{2},\d{3}/
|
||||
)?.[0];
|
||||
if (!timing) continue;
|
||||
|
||||
const [ start, end ] = timing.split(/\s+-->\s+/).map(value =>
|
||||
value.replace(",", ".")
|
||||
);
|
||||
const text = cleanCueText(lines.slice(timingIndex + 1).join("\n"));
|
||||
if (text) cues.push({ start, end, text });
|
||||
}
|
||||
|
||||
return cues;
|
||||
};
|
||||
|
||||
const transcriptLines = cues => {
|
||||
const lines = [];
|
||||
for (const cue of cues) {
|
||||
|
|
@ -83,14 +107,21 @@ const srtTimestamp = timestamp => {
|
|||
|
||||
const escapeMarkdown = value => value.replace(/[\\`*_{}\[\]<>#+.!|~-]/g, "\\$&");
|
||||
|
||||
export const convertCaptions = (vtt, format, metadata = {}) => {
|
||||
if (format === "vtt") {
|
||||
return vtt.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
||||
export const convertCaptions = (input, format, metadata = {}, sourceFormat = "vtt") => {
|
||||
if (sourceFormat === "vtt" && format === "vtt") {
|
||||
return input.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
||||
}
|
||||
|
||||
const cues = parseVtt(vtt);
|
||||
const cues = sourceFormat === "srt" ? parseSrt(input) : parseVtt(input);
|
||||
if (!cues.length) throw new Error("caption file has no cues");
|
||||
|
||||
if (format === "vtt") {
|
||||
return "WEBVTT\n\n" + cues.map(cue => [
|
||||
`${cue.start} --> ${cue.end}`,
|
||||
cue.text,
|
||||
].join("\n")).join("\n\n") + "\n";
|
||||
}
|
||||
|
||||
if (format === "srt") {
|
||||
return cues.map((cue, index) => [
|
||||
index + 1,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const matchesCaptionLanguage = (language, requested) => {
|
|||
export const createCaptionResponse = ({
|
||||
url,
|
||||
format,
|
||||
sourceFormat = "vtt",
|
||||
language,
|
||||
service,
|
||||
id,
|
||||
|
|
@ -27,6 +28,7 @@ export const createCaptionResponse = ({
|
|||
urls: url,
|
||||
headers,
|
||||
captionFormat: format,
|
||||
captionSourceFormat: sourceFormat,
|
||||
captionLanguage: language || "unknown",
|
||||
filenameAttributes: {
|
||||
service,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import test from "node:test";
|
|||
import assert from "node:assert/strict";
|
||||
|
||||
import { selectCaptionTrack } from "../captions.js";
|
||||
import { convertCaptions, parseVtt } from "../caption-formats.js";
|
||||
import { convertCaptions, parseSrt, parseVtt } from "../caption-formats.js";
|
||||
import { apiSchema } from "../../schema.js";
|
||||
|
||||
const sample = `\uFEFFWEBVTT\r
|
||||
|
|
@ -21,6 +21,15 @@ Hello & welcome back\r
|
|||
<v Speaker>Final line</v>\r
|
||||
`;
|
||||
|
||||
const sampleSrt = `1\r
|
||||
00:00:00,000 --> 00:00:02,500\r
|
||||
Hello & welcome\r
|
||||
\r
|
||||
2\r
|
||||
00:00:02,500 --> 00:00:04,000\r
|
||||
Final line\r
|
||||
`;
|
||||
|
||||
test("parseVtt extracts and cleans cues", () => {
|
||||
assert.deepEqual(parseVtt(sample), [
|
||||
{ start: "00:00.000", end: "00:02.500", text: "Hello & welcome" },
|
||||
|
|
@ -29,6 +38,18 @@ test("parseVtt extracts and cleans cues", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test("SRT input converts to transcripts and VTT", () => {
|
||||
assert.equal(parseSrt(sampleSrt).length, 2);
|
||||
assert.equal(
|
||||
convertCaptions(sampleSrt, "txt", {}, "srt"),
|
||||
"Hello & welcome\n\nFinal line\n"
|
||||
);
|
||||
assert.match(
|
||||
convertCaptions(sampleSrt, "vtt", {}, "srt"),
|
||||
/^WEBVTT\n\n00:00:00\.000 --> 00:00:02\.500/
|
||||
);
|
||||
});
|
||||
|
||||
test("plain text collapses growing automatic-caption cues", () => {
|
||||
assert.equal(
|
||||
convertCaptions(sample, "txt"),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export default async function(obj) {
|
|||
|
||||
if (obj.isCaptionOnly) {
|
||||
const caption = play.captions?.find(caption =>
|
||||
caption.format === "webvtt"
|
||||
["webvtt", "srt"].includes(caption.format)
|
||||
&& matchesCaptionLanguage(caption.code, obj.captionLanguage)
|
||||
);
|
||||
if (!caption) return { error: "fetch.empty" };
|
||||
|
|
@ -56,6 +56,7 @@ export default async function(obj) {
|
|||
return createCaptionResponse({
|
||||
url: caption.file,
|
||||
format: obj.captionFormat,
|
||||
sourceFormat: caption.format === "webvtt" ? "vtt" : caption.format,
|
||||
language: caption.code,
|
||||
service: "rutube",
|
||||
id: obj.id,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ export default async function captions(streamInfo, res) {
|
|||
const body = convertCaptions(
|
||||
Buffer.concat(chunks).toString("utf8"),
|
||||
streamInfo.captionFormat,
|
||||
streamInfo.captionMetadata
|
||||
streamInfo.captionMetadata,
|
||||
streamInfo.captionSourceFormat
|
||||
);
|
||||
|
||||
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
},
|
||||
{
|
||||
"name": "standalone transcript",
|
||||
"url": "https://rutube.ru/video/b2f6c27649907c2fde0af411b03825eb/",
|
||||
"canFail": true,
|
||||
"url": "https://rutube.ru/video/76ee456fa0fe79a8e0013b6110cb0168/",
|
||||
"params": {
|
||||
"downloadMode": "captions",
|
||||
"captionFormat": "txt"
|
||||
"captionFormat": "txt",
|
||||
"captionLanguage": "ru"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue