From 6866d565ec7bae2094b368fa8324fc2ed116ea9b Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Thu, 4 Jun 2026 23:37:53 +0000 Subject: [PATCH] =?UTF-8?q?Adiciona=20ferramenta=20VideoMind=20para=20tran?= =?UTF-8?q?scri=C3=A7=C3=A3o=20e=20an=C3=A1lise=20de=20v=C3=ADdeos=20com?= =?UTF-8?q?=20resumo=20customizado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- node_modules/.package-lock.json | 9 + node_modules/youtube-transcript/README.md | 40 +++ .../dist/commonjs/index.d.ts | 70 +++++ .../youtube-transcript/dist/commonjs/index.js | 269 ++++++++++++++++++ .../dist/commonjs/index.js.map | 1 + .../dist/commonjs/package.json | 3 + .../youtube-transcript/dist/esm/index.d.ts | 70 +++++ .../youtube-transcript/dist/esm/index.js | 258 +++++++++++++++++ .../youtube-transcript/dist/esm/index.js.map | 1 + .../youtube-transcript/dist/esm/package.json | 3 + node_modules/youtube-transcript/package.json | 52 ++++ package-lock.json | 12 +- package.json | 3 +- public/app.js | 227 +++++++++++++++ public/index.html | 69 +++++ server.js | 98 +++++++ 16 files changed, 1183 insertions(+), 2 deletions(-) create mode 100644 node_modules/youtube-transcript/README.md create mode 100644 node_modules/youtube-transcript/dist/commonjs/index.d.ts create mode 100644 node_modules/youtube-transcript/dist/commonjs/index.js create mode 100644 node_modules/youtube-transcript/dist/commonjs/index.js.map create mode 100644 node_modules/youtube-transcript/dist/commonjs/package.json create mode 100644 node_modules/youtube-transcript/dist/esm/index.d.ts create mode 100644 node_modules/youtube-transcript/dist/esm/index.js create mode 100644 node_modules/youtube-transcript/dist/esm/index.js.map create mode 100644 node_modules/youtube-transcript/dist/esm/package.json create mode 100644 node_modules/youtube-transcript/package.json diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 59f5782..1707325 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1392,6 +1392,15 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/youtube-transcript": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/youtube-transcript/-/youtube-transcript-1.3.1.tgz", + "integrity": "sha512-NDCjwad113TGybbYF51y9Z4tcwzBHUZWQdF9veULNca18L+FdDbHHtTHIr69WVa3bB90l67S8kN0HtL2JO9fhg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } } } } diff --git a/node_modules/youtube-transcript/README.md b/node_modules/youtube-transcript/README.md new file mode 100644 index 0000000..f433439 --- /dev/null +++ b/node_modules/youtube-transcript/README.md @@ -0,0 +1,40 @@ +# youtube-transcript + +[![npm version](https://badge.fury.io/js/youtube-transcript.svg)](https://badge.fury.io/js/youtube-transcript) + +I wanted to extract a transcript from a youtube video but there was only a python script so I created this to do it in node. +This package use unofficial YTB API so it can be broken over the time if no update appears. + +## Installation + +```bash +$ npm i youtube-transcript +``` + +or + +```bash +$ yarn add youtube-transcript +``` + +or + +```bash +$ pnpm add youtube-transcript +``` + +## Usage + +```js +import { fetchTranscript } from 'youtube-transcript'; + +fetchTranscript('videoId or URL').then(console.log); +``` + +### Methods + +- fetchTranscript(videoId: string [,options: TranscriptConfig]): Promise; + +## License + +**[MIT](LICENSE)** Licensed diff --git a/node_modules/youtube-transcript/dist/commonjs/index.d.ts b/node_modules/youtube-transcript/dist/commonjs/index.d.ts new file mode 100644 index 0000000..7a90938 --- /dev/null +++ b/node_modules/youtube-transcript/dist/commonjs/index.d.ts @@ -0,0 +1,70 @@ +export declare class YoutubeTranscriptError extends Error { + constructor(message: any); +} +export declare class YoutubeTranscriptTooManyRequestError extends YoutubeTranscriptError { + constructor(); +} +export declare class YoutubeTranscriptVideoUnavailableError extends YoutubeTranscriptError { + constructor(videoId: string); +} +export declare class YoutubeTranscriptDisabledError extends YoutubeTranscriptError { + constructor(videoId: string); +} +export declare class YoutubeTranscriptNotAvailableError extends YoutubeTranscriptError { + constructor(videoId: string); +} +export declare class YoutubeTranscriptNotAvailableLanguageError extends YoutubeTranscriptError { + constructor(lang: string, availableLangs: string[], videoId: string); +} +export interface TranscriptConfig { + lang?: string; + fetch?: typeof globalThis.fetch; +} +export interface TranscriptResponse { + text: string; + duration: number; + offset: number; + lang?: string; +} +/** + * Class to retrieve transcript if exist + */ +export declare class YoutubeTranscript { + /** + * Fetch transcript from YTB Video + * @param videoId Video url or video identifier + * @param config Get transcript in a specific language ISO + */ + static fetchTranscript(videoId: string, config?: TranscriptConfig): Promise; + /** + * Fetch transcript via the InnerTube API (Android client context) + */ + private static fetchViaInnerTube; + /** + * Fetch transcript via web page HTML scraping (fallback) + */ + private static fetchViaWebPage; + /** + * Extract a JSON object assigned to a global variable in inline script tags + */ + private static parseInlineJson; + /** + * Given caption tracks, select the right one, fetch and parse the transcript XML + */ + private static fetchTranscriptFromTracks; + /** + * Parse transcript XML, supporting both srv3 format (

) and + * classic format () + */ + private static parseTranscriptXml; + /** + * Decode common HTML entities in transcript text + */ + private static decodeEntities; + /** + * Retrieve video id from url or string + * @param videoId video url or video id + */ + private static retrieveVideoId; +} +export declare function fetchTranscript(videoId: string, config?: TranscriptConfig): Promise; diff --git a/node_modules/youtube-transcript/dist/commonjs/index.js b/node_modules/youtube-transcript/dist/commonjs/index.js new file mode 100644 index 0000000..e91abee --- /dev/null +++ b/node_modules/youtube-transcript/dist/commonjs/index.js @@ -0,0 +1,269 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.YoutubeTranscript = exports.YoutubeTranscriptNotAvailableLanguageError = exports.YoutubeTranscriptNotAvailableError = exports.YoutubeTranscriptDisabledError = exports.YoutubeTranscriptVideoUnavailableError = exports.YoutubeTranscriptTooManyRequestError = exports.YoutubeTranscriptError = void 0; +exports.fetchTranscript = fetchTranscript; +const RE_YOUTUBE = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i; +const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36,gzip(gfe)'; +const RE_XML_TRANSCRIPT = /([^<]*)<\/text>/g; +const INNERTUBE_API_URL = 'https://www.youtube.com/youtubei/v1/player?prettyPrint=false'; +const INNERTUBE_CLIENT_VERSION = '20.10.38'; +const INNERTUBE_CONTEXT = { + client: { + clientName: 'ANDROID', + clientVersion: INNERTUBE_CLIENT_VERSION, + }, +}; +const INNERTUBE_USER_AGENT = `com.google.android.youtube/${INNERTUBE_CLIENT_VERSION} (Linux; U; Android 14)`; +class YoutubeTranscriptError extends Error { + constructor(message) { + super(`[YoutubeTranscript] 🚨 ${message}`); + } +} +exports.YoutubeTranscriptError = YoutubeTranscriptError; +class YoutubeTranscriptTooManyRequestError extends YoutubeTranscriptError { + constructor() { + super('YouTube is receiving too many requests from this IP and now requires solving a captcha to continue'); + } +} +exports.YoutubeTranscriptTooManyRequestError = YoutubeTranscriptTooManyRequestError; +class YoutubeTranscriptVideoUnavailableError extends YoutubeTranscriptError { + constructor(videoId) { + super(`The video is no longer available (${videoId})`); + } +} +exports.YoutubeTranscriptVideoUnavailableError = YoutubeTranscriptVideoUnavailableError; +class YoutubeTranscriptDisabledError extends YoutubeTranscriptError { + constructor(videoId) { + super(`Transcript is disabled on this video (${videoId})`); + } +} +exports.YoutubeTranscriptDisabledError = YoutubeTranscriptDisabledError; +class YoutubeTranscriptNotAvailableError extends YoutubeTranscriptError { + constructor(videoId) { + super(`No transcripts are available for this video (${videoId})`); + } +} +exports.YoutubeTranscriptNotAvailableError = YoutubeTranscriptNotAvailableError; +class YoutubeTranscriptNotAvailableLanguageError extends YoutubeTranscriptError { + constructor(lang, availableLangs, videoId) { + super(`No transcripts are available in ${lang} this video (${videoId}). Available languages: ${availableLangs.join(', ')}`); + } +} +exports.YoutubeTranscriptNotAvailableLanguageError = YoutubeTranscriptNotAvailableLanguageError; +/** + * Class to retrieve transcript if exist + */ +class YoutubeTranscript { + /** + * Fetch transcript from YTB Video + * @param videoId Video url or video identifier + * @param config Get transcript in a specific language ISO + */ + static async fetchTranscript(videoId, config) { + const identifier = this.retrieveVideoId(videoId); + // Try InnerTube API first + const innerTubeResult = await this.fetchViaInnerTube(identifier, config); + if (innerTubeResult) { + return innerTubeResult; + } + // Fall back to HTML scraping + return this.fetchViaWebPage(identifier, videoId, config); + } + /** + * Fetch transcript via the InnerTube API (Android client context) + */ + static async fetchViaInnerTube(identifier, config) { + try { + const fetchFn = config?.fetch ?? fetch; + const resp = await fetchFn(INNERTUBE_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': INNERTUBE_USER_AGENT, + }, + body: JSON.stringify({ + context: INNERTUBE_CONTEXT, + videoId: identifier, + }), + }); + if (!resp.ok) + return undefined; + const data = await resp.json(); + const captionTracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks; + if (!Array.isArray(captionTracks) || captionTracks.length === 0) { + return undefined; + } + return this.fetchTranscriptFromTracks(captionTracks, identifier, config); + } + catch { + return undefined; + } + } + /** + * Fetch transcript via web page HTML scraping (fallback) + */ + static async fetchViaWebPage(identifier, originalVideoId, config) { + const fetchFn = config?.fetch ?? fetch; + const videoPageResponse = await fetchFn(`https://www.youtube.com/watch?v=${identifier}`, { + headers: { + ...(config?.lang && { 'Accept-Language': config.lang }), + 'User-Agent': USER_AGENT, + }, + }); + const videoPageBody = await videoPageResponse.text(); + if (videoPageBody.includes('class="g-recaptcha"')) { + throw new YoutubeTranscriptTooManyRequestError(); + } + if (!videoPageBody.includes('"playabilityStatus":')) { + throw new YoutubeTranscriptVideoUnavailableError(originalVideoId); + } + const playerResponse = this.parseInlineJson(videoPageBody, 'ytInitialPlayerResponse'); + const captionTracks = playerResponse?.captions?.playerCaptionsTracklistRenderer?.captionTracks; + if (!Array.isArray(captionTracks) || captionTracks.length === 0) { + throw new YoutubeTranscriptDisabledError(originalVideoId); + } + return this.fetchTranscriptFromTracks(captionTracks, originalVideoId, config); + } + /** + * Extract a JSON object assigned to a global variable in inline script tags + */ + static parseInlineJson(html, globalName) { + const startToken = `var ${globalName} = `; + const startIndex = html.indexOf(startToken); + if (startIndex === -1) + return null; + const jsonStart = startIndex + startToken.length; + let depth = 0; + for (let i = jsonStart; i < html.length; i++) { + if (html[i] === '{') + depth++; + else if (html[i] === '}') { + depth--; + if (depth === 0) { + try { + return JSON.parse(html.slice(jsonStart, i + 1)); + } + catch { + return null; + } + } + } + } + return null; + } + /** + * Given caption tracks, select the right one, fetch and parse the transcript XML + */ + static async fetchTranscriptFromTracks(captionTracks, videoId, config) { + if (config?.lang && + !captionTracks.some((track) => track.languageCode === config?.lang)) { + throw new YoutubeTranscriptNotAvailableLanguageError(config?.lang, captionTracks.map((track) => track.languageCode), videoId); + } + const track = config?.lang + ? captionTracks.find((track) => track.languageCode === config?.lang) + : captionTracks[0]; + const transcriptURL = track.baseUrl; + // Validate URL to prevent SSRF + try { + const captionUrl = new URL(transcriptURL); + if (!captionUrl.hostname.endsWith('.youtube.com')) { + throw new YoutubeTranscriptNotAvailableError(videoId); + } + } + catch (e) { + if (e instanceof YoutubeTranscriptError) + throw e; + throw new YoutubeTranscriptNotAvailableError(videoId); + } + const fetchFn = config?.fetch ?? fetch; + const transcriptResponse = await fetchFn(transcriptURL, { + headers: { + ...(config?.lang && { 'Accept-Language': config.lang }), + 'User-Agent': USER_AGENT, + }, + }); + if (!transcriptResponse.ok) { + throw new YoutubeTranscriptNotAvailableError(videoId); + } + const transcriptBody = await transcriptResponse.text(); + const lang = config?.lang ?? captionTracks[0].languageCode; + return this.parseTranscriptXml(transcriptBody, lang); + } + /** + * Parse transcript XML, supporting both srv3 format (

) and + * classic format () + */ + static parseTranscriptXml(xml, lang) { + const results = []; + // Try srv3 format first:

word...

+ const pRegex = /]*>([\s\S]*?)<\/p>/g; + let match; + while ((match = pRegex.exec(xml)) !== null) { + const startMs = parseInt(match[1], 10); + const durMs = parseInt(match[2], 10); + const inner = match[3]; + let text = ''; + const sRegex = /]*>([^<]*)<\/s>/g; + let sMatch; + while ((sMatch = sRegex.exec(inner)) !== null) { + text += sMatch[1]; + } + if (!text) { + text = inner.replace(/<[^>]+>/g, ''); + } + text = this.decodeEntities(text).trim(); + if (text) { + results.push({ + text, + duration: durMs, + offset: startMs, + lang, + }); + } + } + if (results.length > 0) + return results; + // Fall back to classic format: content + const classicResults = [...xml.matchAll(RE_XML_TRANSCRIPT)]; + return classicResults.map((result) => ({ + text: this.decodeEntities(result[3]), + duration: parseFloat(result[2]), + offset: parseFloat(result[1]), + lang, + })); + } + /** + * Decode common HTML entities in transcript text + */ + static decodeEntities(text) { + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16))) + .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10))); + } + /** + * Retrieve video id from url or string + * @param videoId video url or video id + */ + static retrieveVideoId(videoId) { + if (videoId.length === 11) { + return videoId; + } + const matchId = videoId.match(RE_YOUTUBE); + if (matchId && matchId.length) { + return matchId[1]; + } + throw new YoutubeTranscriptError('Impossible to retrieve Youtube video ID.'); + } +} +exports.YoutubeTranscript = YoutubeTranscript; +//^ class is kept for backward compatibility +function fetchTranscript(videoId, config) { + return YoutubeTranscript.fetchTranscript(videoId, config); +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/node_modules/youtube-transcript/dist/commonjs/index.js.map b/node_modules/youtube-transcript/dist/commonjs/index.js.map new file mode 100644 index 0000000..dba6229 --- /dev/null +++ b/node_modules/youtube-transcript/dist/commonjs/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/node_modules/youtube-transcript/dist/commonjs/package.json b/node_modules/youtube-transcript/dist/commonjs/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/node_modules/youtube-transcript/dist/commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/node_modules/youtube-transcript/dist/esm/index.d.ts b/node_modules/youtube-transcript/dist/esm/index.d.ts new file mode 100644 index 0000000..7a90938 --- /dev/null +++ b/node_modules/youtube-transcript/dist/esm/index.d.ts @@ -0,0 +1,70 @@ +export declare class YoutubeTranscriptError extends Error { + constructor(message: any); +} +export declare class YoutubeTranscriptTooManyRequestError extends YoutubeTranscriptError { + constructor(); +} +export declare class YoutubeTranscriptVideoUnavailableError extends YoutubeTranscriptError { + constructor(videoId: string); +} +export declare class YoutubeTranscriptDisabledError extends YoutubeTranscriptError { + constructor(videoId: string); +} +export declare class YoutubeTranscriptNotAvailableError extends YoutubeTranscriptError { + constructor(videoId: string); +} +export declare class YoutubeTranscriptNotAvailableLanguageError extends YoutubeTranscriptError { + constructor(lang: string, availableLangs: string[], videoId: string); +} +export interface TranscriptConfig { + lang?: string; + fetch?: typeof globalThis.fetch; +} +export interface TranscriptResponse { + text: string; + duration: number; + offset: number; + lang?: string; +} +/** + * Class to retrieve transcript if exist + */ +export declare class YoutubeTranscript { + /** + * Fetch transcript from YTB Video + * @param videoId Video url or video identifier + * @param config Get transcript in a specific language ISO + */ + static fetchTranscript(videoId: string, config?: TranscriptConfig): Promise; + /** + * Fetch transcript via the InnerTube API (Android client context) + */ + private static fetchViaInnerTube; + /** + * Fetch transcript via web page HTML scraping (fallback) + */ + private static fetchViaWebPage; + /** + * Extract a JSON object assigned to a global variable in inline script tags + */ + private static parseInlineJson; + /** + * Given caption tracks, select the right one, fetch and parse the transcript XML + */ + private static fetchTranscriptFromTracks; + /** + * Parse transcript XML, supporting both srv3 format (

) and + * classic format () + */ + private static parseTranscriptXml; + /** + * Decode common HTML entities in transcript text + */ + private static decodeEntities; + /** + * Retrieve video id from url or string + * @param videoId video url or video id + */ + private static retrieveVideoId; +} +export declare function fetchTranscript(videoId: string, config?: TranscriptConfig): Promise; diff --git a/node_modules/youtube-transcript/dist/esm/index.js b/node_modules/youtube-transcript/dist/esm/index.js new file mode 100644 index 0000000..b8d6d0f --- /dev/null +++ b/node_modules/youtube-transcript/dist/esm/index.js @@ -0,0 +1,258 @@ +const RE_YOUTUBE = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i; +const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36,gzip(gfe)'; +const RE_XML_TRANSCRIPT = /([^<]*)<\/text>/g; +const INNERTUBE_API_URL = 'https://www.youtube.com/youtubei/v1/player?prettyPrint=false'; +const INNERTUBE_CLIENT_VERSION = '20.10.38'; +const INNERTUBE_CONTEXT = { + client: { + clientName: 'ANDROID', + clientVersion: INNERTUBE_CLIENT_VERSION, + }, +}; +const INNERTUBE_USER_AGENT = `com.google.android.youtube/${INNERTUBE_CLIENT_VERSION} (Linux; U; Android 14)`; +export class YoutubeTranscriptError extends Error { + constructor(message) { + super(`[YoutubeTranscript] 🚨 ${message}`); + } +} +export class YoutubeTranscriptTooManyRequestError extends YoutubeTranscriptError { + constructor() { + super('YouTube is receiving too many requests from this IP and now requires solving a captcha to continue'); + } +} +export class YoutubeTranscriptVideoUnavailableError extends YoutubeTranscriptError { + constructor(videoId) { + super(`The video is no longer available (${videoId})`); + } +} +export class YoutubeTranscriptDisabledError extends YoutubeTranscriptError { + constructor(videoId) { + super(`Transcript is disabled on this video (${videoId})`); + } +} +export class YoutubeTranscriptNotAvailableError extends YoutubeTranscriptError { + constructor(videoId) { + super(`No transcripts are available for this video (${videoId})`); + } +} +export class YoutubeTranscriptNotAvailableLanguageError extends YoutubeTranscriptError { + constructor(lang, availableLangs, videoId) { + super(`No transcripts are available in ${lang} this video (${videoId}). Available languages: ${availableLangs.join(', ')}`); + } +} +/** + * Class to retrieve transcript if exist + */ +export class YoutubeTranscript { + /** + * Fetch transcript from YTB Video + * @param videoId Video url or video identifier + * @param config Get transcript in a specific language ISO + */ + static async fetchTranscript(videoId, config) { + const identifier = this.retrieveVideoId(videoId); + // Try InnerTube API first + const innerTubeResult = await this.fetchViaInnerTube(identifier, config); + if (innerTubeResult) { + return innerTubeResult; + } + // Fall back to HTML scraping + return this.fetchViaWebPage(identifier, videoId, config); + } + /** + * Fetch transcript via the InnerTube API (Android client context) + */ + static async fetchViaInnerTube(identifier, config) { + try { + const fetchFn = config?.fetch ?? fetch; + const resp = await fetchFn(INNERTUBE_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': INNERTUBE_USER_AGENT, + }, + body: JSON.stringify({ + context: INNERTUBE_CONTEXT, + videoId: identifier, + }), + }); + if (!resp.ok) + return undefined; + const data = await resp.json(); + const captionTracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks; + if (!Array.isArray(captionTracks) || captionTracks.length === 0) { + return undefined; + } + return this.fetchTranscriptFromTracks(captionTracks, identifier, config); + } + catch { + return undefined; + } + } + /** + * Fetch transcript via web page HTML scraping (fallback) + */ + static async fetchViaWebPage(identifier, originalVideoId, config) { + const fetchFn = config?.fetch ?? fetch; + const videoPageResponse = await fetchFn(`https://www.youtube.com/watch?v=${identifier}`, { + headers: { + ...(config?.lang && { 'Accept-Language': config.lang }), + 'User-Agent': USER_AGENT, + }, + }); + const videoPageBody = await videoPageResponse.text(); + if (videoPageBody.includes('class="g-recaptcha"')) { + throw new YoutubeTranscriptTooManyRequestError(); + } + if (!videoPageBody.includes('"playabilityStatus":')) { + throw new YoutubeTranscriptVideoUnavailableError(originalVideoId); + } + const playerResponse = this.parseInlineJson(videoPageBody, 'ytInitialPlayerResponse'); + const captionTracks = playerResponse?.captions?.playerCaptionsTracklistRenderer?.captionTracks; + if (!Array.isArray(captionTracks) || captionTracks.length === 0) { + throw new YoutubeTranscriptDisabledError(originalVideoId); + } + return this.fetchTranscriptFromTracks(captionTracks, originalVideoId, config); + } + /** + * Extract a JSON object assigned to a global variable in inline script tags + */ + static parseInlineJson(html, globalName) { + const startToken = `var ${globalName} = `; + const startIndex = html.indexOf(startToken); + if (startIndex === -1) + return null; + const jsonStart = startIndex + startToken.length; + let depth = 0; + for (let i = jsonStart; i < html.length; i++) { + if (html[i] === '{') + depth++; + else if (html[i] === '}') { + depth--; + if (depth === 0) { + try { + return JSON.parse(html.slice(jsonStart, i + 1)); + } + catch { + return null; + } + } + } + } + return null; + } + /** + * Given caption tracks, select the right one, fetch and parse the transcript XML + */ + static async fetchTranscriptFromTracks(captionTracks, videoId, config) { + if (config?.lang && + !captionTracks.some((track) => track.languageCode === config?.lang)) { + throw new YoutubeTranscriptNotAvailableLanguageError(config?.lang, captionTracks.map((track) => track.languageCode), videoId); + } + const track = config?.lang + ? captionTracks.find((track) => track.languageCode === config?.lang) + : captionTracks[0]; + const transcriptURL = track.baseUrl; + // Validate URL to prevent SSRF + try { + const captionUrl = new URL(transcriptURL); + if (!captionUrl.hostname.endsWith('.youtube.com')) { + throw new YoutubeTranscriptNotAvailableError(videoId); + } + } + catch (e) { + if (e instanceof YoutubeTranscriptError) + throw e; + throw new YoutubeTranscriptNotAvailableError(videoId); + } + const fetchFn = config?.fetch ?? fetch; + const transcriptResponse = await fetchFn(transcriptURL, { + headers: { + ...(config?.lang && { 'Accept-Language': config.lang }), + 'User-Agent': USER_AGENT, + }, + }); + if (!transcriptResponse.ok) { + throw new YoutubeTranscriptNotAvailableError(videoId); + } + const transcriptBody = await transcriptResponse.text(); + const lang = config?.lang ?? captionTracks[0].languageCode; + return this.parseTranscriptXml(transcriptBody, lang); + } + /** + * Parse transcript XML, supporting both srv3 format (

) and + * classic format () + */ + static parseTranscriptXml(xml, lang) { + const results = []; + // Try srv3 format first:

word...

+ const pRegex = /]*>([\s\S]*?)<\/p>/g; + let match; + while ((match = pRegex.exec(xml)) !== null) { + const startMs = parseInt(match[1], 10); + const durMs = parseInt(match[2], 10); + const inner = match[3]; + let text = ''; + const sRegex = /]*>([^<]*)<\/s>/g; + let sMatch; + while ((sMatch = sRegex.exec(inner)) !== null) { + text += sMatch[1]; + } + if (!text) { + text = inner.replace(/<[^>]+>/g, ''); + } + text = this.decodeEntities(text).trim(); + if (text) { + results.push({ + text, + duration: durMs, + offset: startMs, + lang, + }); + } + } + if (results.length > 0) + return results; + // Fall back to classic format: content + const classicResults = [...xml.matchAll(RE_XML_TRANSCRIPT)]; + return classicResults.map((result) => ({ + text: this.decodeEntities(result[3]), + duration: parseFloat(result[2]), + offset: parseFloat(result[1]), + lang, + })); + } + /** + * Decode common HTML entities in transcript text + */ + static decodeEntities(text) { + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16))) + .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10))); + } + /** + * Retrieve video id from url or string + * @param videoId video url or video id + */ + static retrieveVideoId(videoId) { + if (videoId.length === 11) { + return videoId; + } + const matchId = videoId.match(RE_YOUTUBE); + if (matchId && matchId.length) { + return matchId[1]; + } + throw new YoutubeTranscriptError('Impossible to retrieve Youtube video ID.'); + } +} +//^ class is kept for backward compatibility +export function fetchTranscript(videoId, config) { + return YoutubeTranscript.fetchTranscript(videoId, config); +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/node_modules/youtube-transcript/dist/esm/index.js.map b/node_modules/youtube-transcript/dist/esm/index.js.map new file mode 100644 index 0000000..f2606b5 --- /dev/null +++ b/node_modules/youtube-transcript/dist/esm/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/node_modules/youtube-transcript/dist/esm/package.json b/node_modules/youtube-transcript/dist/esm/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/node_modules/youtube-transcript/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/node_modules/youtube-transcript/package.json b/node_modules/youtube-transcript/package.json new file mode 100644 index 0000000..9e97002 --- /dev/null +++ b/node_modules/youtube-transcript/package.json @@ -0,0 +1,52 @@ +{ + "name": "youtube-transcript", + "version": "1.3.1", + "description": "Fetch transcript from a youtube video", + "type": "module", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/commonjs/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + } + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + } + }, + "author": "Kakulukian", + "keywords": [ + "youtube", + "transcript" + ], + "license": "MIT", + "devDependencies": { + "tshy": "^3.0.0", + "typescript": "^5.7.0" + }, + "files": [ + "dist" + ], + "repository": "https://github.com/Kakulukian/youtube-transcript.git", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/Kakulukian/youtube-transcript", + "engines": { + "node": ">=18.0.0" + }, + "scripts": { + "build": "tshy" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 55202cd..076df16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "express": "^4.19.2", "msedge-tts": "^2.0.5", "multer": "^2.1.1", - "pg": "^8.21.0" + "pg": "^8.21.0", + "youtube-transcript": "^1.3.1" } }, "node_modules/accepts": { @@ -1404,6 +1405,15 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/youtube-transcript": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/youtube-transcript/-/youtube-transcript-1.3.1.tgz", + "integrity": "sha512-NDCjwad113TGybbYF51y9Z4tcwzBHUZWQdF9veULNca18L+FdDbHHtTHIr69WVa3bB90l67S8kN0HtL2JO9fhg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } } } } diff --git a/package.json b/package.json index 19f492e..fcafab0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "express": "^4.19.2", "msedge-tts": "^2.0.5", "multer": "^2.1.1", - "pg": "^8.21.0" + "pg": "^8.21.0", + "youtube-transcript": "^1.3.1" } } diff --git a/public/app.js b/public/app.js index 383bd50..8de8dd4 100644 --- a/public/app.js +++ b/public/app.js @@ -4489,6 +4489,233 @@ const initApp = () => { } }); } + + // ============================================================ + // VIDEOMIND (TRANSCRIÇÃO E ANÁLISE DE VÍDEO PEDAGÓGICO) + // ============================================================ + const videoMindModal = document.getElementById('videoMindModal'); + const btnCloseVideoMindModal = document.getElementById('btnCloseVideoMindModal'); + const barBtnVideoMind = document.getElementById('barBtnVideoMind'); + + const btnGenerateVideoMind = document.getElementById('btnGenerateVideoMind'); + const videoMindUrlInput = document.getElementById('videoMindUrlInput'); + const videoMindLoader = document.getElementById('videoMindLoader'); + const videoMindLoaderText = document.getElementById('videoMindLoaderText'); + const videoMindResultArea = document.getElementById('videoMindResultArea'); + + const videoMindThumbnail = document.getElementById('videoMindThumbnail'); + const videoMindTitle = document.getElementById('videoMindTitle'); + const videoMindAuthor = document.getElementById('videoMindAuthor'); + const videoMindReportArea = document.getElementById('videoMindReportArea'); + + const btnCopyVideoMindReport = document.getElementById('btnCopyVideoMindReport'); + const btnDownloadVideoMindReport = document.getElementById('btnDownloadVideoMindReport'); + const btnPdfVideoMindReport = document.getElementById('btnPdfVideoMindReport'); + + let selectedVideoMindLines = 20; + let activeVideoMindReport = null; + + // Listener para selecionar o tamanho do resumo + document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + selectedVideoMindLines = parseInt(btn.dataset.lines) || 20; + }); + }); + + // Abrir modal do VideoMind + if (barBtnVideoMind) { + barBtnVideoMind.addEventListener('click', () => { + videoMindModal.style.display = 'flex'; + videoMindUrlInput.value = ''; + videoMindResultArea.style.display = 'none'; + videoMindReportArea.innerHTML = ''; + if (videoMindLoader) videoMindLoader.style.display = 'none'; + btnGenerateVideoMind.disabled = false; + activeVideoMindReport = null; + }); + } + + // Fechar modal + if (btnCloseVideoMindModal) { + btnCloseVideoMindModal.addEventListener('click', () => { + videoMindModal.style.display = 'none'; + }); + } + + window.addEventListener('click', (e) => { + if (e.target === videoMindModal) { + videoMindModal.style.display = 'none'; + } + }); + + // Ação de analisar vídeo + if (btnGenerateVideoMind) { + btnGenerateVideoMind.addEventListener('click', async () => { + const url = videoMindUrlInput.value.trim(); + if (!url) { + alert('Por favor, insira o link de um vídeo do YouTube.'); + return; + } + + btnGenerateVideoMind.disabled = true; + videoMindLoader.style.display = 'flex'; + videoMindResultArea.style.display = 'none'; + videoMindLoaderText.textContent = 'Buscando metadados do vídeo...'; + + let statusMsgIdx = 0; + const statusMessages = [ + 'Buscando legenda automática do vídeo...', + 'Carregando a transcrição completa...', + 'Enviando transcrição para a IA...', + 'Pedagoga IA analisando o conteúdo...', + 'Alinhando temas do vídeo com a BNCC...', + 'Gerando resumo de ' + selectedVideoMindLines + ' linhas...', + 'Criando propostas de atividades pedagógicas...', + 'Finalizando o parecer pedagógico...' + ]; + + const statusInterval = setInterval(() => { + if (statusMsgIdx < statusMessages.length) { + videoMindLoaderText.textContent = statusMessages[statusMsgIdx]; + statusMsgIdx++; + } else { + videoMindLoaderText.textContent = 'Ajustando formatação da análise...'; + } + }, 4000); + + try { + const response = await fetch('/api/videomind/analyze', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url, + lines: selectedVideoMindLines + }) + }); + + clearInterval(statusInterval); + + if (!response.ok) { + const errData = await response.json(); + throw new Error(errData.error || 'Erro desconhecido'); + } + + const data = await response.json(); + activeVideoMindReport = data; + + // Preencher informações do vídeo + videoMindTitle.textContent = data.metadata.title; + videoMindAuthor.textContent = 'Canal: ' + data.metadata.author; + videoMindThumbnail.src = data.metadata.thumbnail; + + // Renderizar Markdown + if (window.marked) { + videoMindReportArea.innerHTML = marked.parse(data.report); + } else { + videoMindReportArea.textContent = data.report; + } + + videoMindLoader.style.display = 'none'; + videoMindResultArea.style.display = 'flex'; + + } catch (err) { + clearInterval(statusInterval); + console.error('Erro no VideoMind:', err); + alert('Erro ao analisar vídeo: ' + err.message); + videoMindLoader.style.display = 'none'; + btnGenerateVideoMind.disabled = false; + } + }); + } + + // Copiar Parecer + if (btnCopyVideoMindReport) { + btnCopyVideoMindReport.addEventListener('click', () => { + if (activeVideoMindReport && activeVideoMindReport.report) { + navigator.clipboard.writeText(activeVideoMindReport.report) + .then(() => { + btnCopyVideoMindReport.textContent = '✅ Copiado!'; + setTimeout(() => { + btnCopyVideoMindReport.textContent = '📋 Copiar Parecer'; + }, 2000); + }) + .catch(err => { + console.error('Falha ao copiar:', err); + }); + } + }); + } + + // Baixar relatório TXT + if (btnDownloadVideoMindReport) { + btnDownloadVideoMindReport.addEventListener('click', () => { + if (activeVideoMindReport && activeVideoMindReport.report) { + const title = activeVideoMindReport.metadata.title || 'analise_video'; + const blob = new Blob([activeVideoMindReport.report], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `VideoMind_${title.replace(/\s+/g, '_').toLowerCase()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + }); + } + + // Baixar relatório PDF + if (btnPdfVideoMindReport) { + btnPdfVideoMindReport.addEventListener('click', () => { + if (!activeVideoMindReport) return; + + const printWindow = window.open('', '_blank'); + printWindow.document.write(` + + + VideoMind - Parecer Pedagógico + + + +

🎬 VideoMind - Análise & Parecer Pedagógico

+
+ +
+
Vídeo: ${activeVideoMindReport.metadata.title}
+
Canal: ${activeVideoMindReport.metadata.author}
+
Resumo Escolhido: ${selectedVideoMindLines} Linhas
+
+
+
${window.marked ? marked.parse(activeVideoMindReport.report) : activeVideoMindReport.report}
+ + + + `); + printWindow.document.close(); + }); + } }; // Player de áudio personalizado global para as mídias geradas diff --git a/public/index.html b/public/index.html index 2b350e9..015454d 100644 --- a/public/index.html +++ b/public/index.html @@ -237,6 +237,9 @@ + @@ -938,6 +941,72 @@ + + + diff --git a/server.js b/server.js index fe928b7..ed9068f 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,7 @@ const cookieParser = require('cookie-parser'); const path = require('path'); const multer = require('multer'); const { MsEdgeTTS, OUTPUT_FORMAT } = require('msedge-tts'); +const { YoutubeTranscript } = require('youtube-transcript'); const app = express(); const PORT = process.env.PORT || 3000; @@ -691,6 +692,103 @@ app.delete('/api/comics/projects/:id', requireAuth, async (req, res) => { } }); +// ============================================================ +// ROTAS DO VIDEOMIND (TRANSCRIÇÃO E ANÁLISE DE VÍDEO) +// ============================================================ +function getYouTubeId(url) { + const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; + const match = url.match(regExp); + return (match && match[2].length === 11) ? match[2] : null; +} + +app.post('/api/videomind/analyze', requireAuth, async (req, res) => { + const { url, lines } = req.body; + + if (!url) { + return res.status(400).json({ error: 'A URL do vídeo é obrigatória.' }); + } + + const videoId = getYouTubeId(url); + if (!videoId) { + return res.status(400).json({ error: 'A URL inserida não é uma URL válida do YouTube.' }); + } + + const numLines = parseInt(lines) || 20; + let transcriptText = ''; + let metadata = { + title: 'Vídeo do YouTube', + author: 'Desconhecido', + thumbnail: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg` + }; + + try { + // Tenta obter metadados via oEmbed oficial do YouTube + const oEmbedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`; + const oEmbedResp = await fetch(oEmbedUrl); + if (oEmbedResp.ok) { + const oEmbedData = await oEmbedResp.json(); + metadata.title = oEmbedData.title || metadata.title; + metadata.author = oEmbedData.author_name || metadata.author; + metadata.thumbnail = oEmbedData.thumbnail_url || metadata.thumbnail; + } + } catch (metaErr) { + console.error('Erro ao buscar metadados do oEmbed:', metaErr); + } + + try { + // Tenta obter legenda + const transcriptObj = await YoutubeTranscript.fetchTranscript(videoId); + transcriptText = transcriptObj.map(t => t.text).join(' '); + } catch (transcErr) { + console.error('Erro ao obter transcrição:', transcErr); + } + + if (!transcriptText) { + return res.status(400).json({ + error: 'Não foi possível extrair as legendas/transcrição automática deste vídeo. Certifique-se de que o vídeo possui legendas ou transcrição automática disponíveis no YouTube e tente novamente.' + }); + } + + try { + const prompt = `Analise a transcrição deste vídeo intitulado "${metadata.title}" (Canal: ${metadata.author}): +Transcrição do vídeo: +""" +${transcriptText} +""" + +Instruções para o conteúdo de saída: +1. Escreva um RESUMO detalhado do vídeo. Esse resumo precisa ter EXATAMENTE ou APROXIMADAMENTE ${numLines} linhas de texto. Seja extremamente fiel aos fatos relatados. +2. Faça uma ANÁLISE PEDAGÓGICA COMPLEMENTAR detalhando: + - Relevância do conteúdo para fins educacionais. + - Indicação de faixa etária/segmento dos alunos recomendado. + - Conexão direta com a BNCC (mencione os códigos de competências, habilidades ou campos de experiência correspondentes). + - Sugestões práticas de aplicação em sala de aula (atividades pedagógicas, dinâmicas de grupo ou perguntas para debate com os alunos). + +Retorne a resposta estruturada estritamente em Markdown. Separe as seções usando títulos claros: +# Resumo Pedagógico (${numLines} Linhas) +[Insira o resumo aqui, ocupando aproximadamente ${numLines} linhas de texto] + +# Análise Pedagógica e Sugestões Práticas +[Insira a análise pedagógica, os pontos da BNCC e as sugestões de atividades práticas aqui]`; + + const aiResponse = await callMinimax({ + system: "Você é um pedagogo experiente e especialista em planejamento de aulas no Brasil, alinhado à Base Nacional Comum Curricular (BNCC).", + messages: [{ role: 'user', content: prompt }], + temperature: 0.5, + max_tokens: 3000 + }); + + res.json({ + success: true, + metadata, + report: aiResponse.text || '' + }); + } catch (err) { + console.error('Erro ao processar análise do VideoMind:', err); + res.status(500).json({ error: 'Erro ao gerar análise com a IA: ' + err.message }); + } +}); + // Rota GET para obter configurações do agente app.get('/api/agent-config', requireAuth, (req, res) => { const config = readAgentConfig();