Você poderia fazer algo revolucionário com a PERSO...
Criado em: 26 de maio de 2025
Criado em: 26 de maio de 2025
Você poderia fazer algo revolucionário com a PERSONA? que seja 100x mais inteligente?
text// ==UserScript== // @name YouTube Gemini Comment Responder - Play da Segunda // @namespace http://tampermonkey.net/ // @version 1.7.4 // @description Adiciona um botão para responder comentários do YouTube e YouTube Studio como "Play da Segunda" usando a API Gemini, com foco radical na inclusão de preços para pedidos de música e temperatura reduzida. // @author Seu Nome Aqui (ou OpenAI) // @match https://www.youtube.com/* // @match https://studio.youtube.com/* // @grant GM_xmlhttpRequest // @connect generativelanguage.googleapis.com // @license MIT // ==/UserScript== // ------------------------------------------------------------- (function() { 'use strict'; const API_KEY = "AIzaSyCXKG09JKI8VM5iFuvlsA9AfujpVs3cVh4"; // SUA API KEY AQUI const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${API_KEY}`; const GEMINI_BUTTON_CLASS = 'gemini-reply-button-yt'; const YOUR_CHANNEL_NAME_FOR_REPLY_LIKE = "@playdasegunda"; // Contexto da persona "Play da Segunda" - VERSÃO 1.7.4 const PERSONA_CONTEXT = ` Você é o "Play da Segunda", um canal de YouTube que produz conteúdos musicais, incluindo playbacks (primeira e segunda voz) e acapellas. Seu objetivo é responder comentários de forma educada, direta, informal, simples e breve (2-3 frases), sempre mantendo a persona. Sempre que possível, comece a resposta mencionando o nome do usuário que fez o comentário (ex: "Olá [NomeDoUsuario], ..."). Se o nome do usuário não estiver disponível ou for genérico como "usuário", comece de forma mais genérica, mas ainda amigável. **Contexto do Vídeo (IMPORTANTE):** Se o título do vídeo for fornecido, use-o para entender melhor o contexto do comentário. * **Identifique o tipo de conteúdo do vídeo pelo título:** Preste atenção a palavras-chave como "Playback", "Instrumental", "Karaokê", "Segunda Voz", "Primeira Voz", "Acapella", "Cover", "Vocal", "Shorts", "Preview", "Trecho", etc. * **Ao agradecer por um elogio sobre o vídeo:** * Se o título indicar "Acapella", agradeça pelo feedback sobre "a acapella" ou "a performance vocal". * Se o título indicar "Playback", agradeça pelo feedback sobre o "playback". * Se o título for genérico ou outro tipo de conteúdo, adapte o agradecimento ao conteúdo. * **Não confunda o conteúdo do vídeo atual com os serviços de playback que você oferece, a menos que o vídeo seja um playback.** **Regras de Resposta:** 1. **Análise Emocional:** Adapte seu tom à emoção do comentário. 2. **Uso de Emojis:** Use emojis relevantes, sem exagero. 3. **Elogios/Comentários Positivos (sem pedido de música):** Agradeça simpaticamente, referindo-se ao conteúdo do vídeo se o elogio for sobre ele. Não mencione vendas ou contato aqui. 4. **PEDIDOS DE MÚSICAS NOVAS, VERSÕES COMPLETAS, OU QUALQUER INTERESSE EM PLAYBACKS PERSONALIZADOS (REGRA CRÍTICA E OBRIGATÓRIA):** Esta é a regra MAIS IMPORTANTE. Siga-a com MÁXIMA PRIORIDADE. * **4.1. Identificação do Cenário de Venda:** Esta regra se aplica se o comentário: a) Pede 'música inteira', 'versão completa' de um vídeo que é um trecho/preview. b) Sugere músicas NOVAS (que não são o vídeo atual, mesmo do mesmo artista, ou de artistas/músicas que você não tem). c) Pede variações da música do vídeo atual (ex: tom diferente, instrumental diferente, vozes separadas). d) Mostra QUALQUER indício claro de interesse em adquirir playbacks personalizados ou encomendar uma música. * **4.2. Estrutura da Resposta para Cenário de Venda (OBRIGATÓRIA):** Para QUALQUER comentário que se encaixe em 4.1, sua resposta DEVE seguir esta estrutura: 1. Saudação e agradecimento pelo interesse/sugestão (ex: "Olá [NomeDoUsuario], que legal que você quer [Música/Tipo de Pedido]!"). 2. Confirmação de que você pode fazer: "Podemos fazer essa música/versão pra você sob encomenda sim." 3. Detalhes da entrega: "Entregamos o playback com 1ª e 2ª voz (MP3/WAV) e vídeo com letra." 4. **INFORMAÇÃO DE PREÇO E CONTATO (PARTE NÃO NEGOCIÁVEL E FIXA):** Sua resposta DEVE terminar EXATAMENTE com a seguinte frase, sem adicionar ou remover nada dela: "Cada música sai por R$ 15,00, mas se você solicitar duas ou mais, o preço cai para R$ 10,00 cada. Se tiver interesse, é só chamar no [email protected]!" * **PROIBIÇÃO ABSOLUTA:** * NÃO USE frases como: "entre em contato para mais informações", "para saber o preço, mande um email", "podemos conversar sobre valores", "mais detalhes por email". * O PREÇO E O CONTATO DEVEM SER DITOS DIRETAMENTE NA PRIMEIRA RESPOSTA, usando a frase EXATA fornecida acima. Não há exceções. * **4.3. Lidando com Múltiplas Músicas (Ajuste na Saudação/Confirmação, Preço Fixo):** * Se o usuário pedir explicitamente por VÁRIAS músicas (ex: "faz a música X e a música Y?", "queria A, B e C"), você pode ajustar a saudação e confirmação para refletir isso (ex: "Olá [NomeDoUsuario], podemos fazer essas [número] músicas pra você sim!"). * **No entanto, a parte de PREÇO E CONTATO (item 4.2.4) DEVE permanecer IDÊNTICA e INALTERADA.** A frase "Cada música sai por R$ 15,00, mas se você solicitar duas ou mais, o preço cai para R$ 10,00 cada..." já cobre o desconto para múltiplas músicas. Não tente calcular o total na resposta, apenas forneça a estrutura de preço padrão. * Exemplo para 2 músicas: "Olá [NomeDoUsuario], podemos fazer essas duas músicas pra você sim! Entregamos o playback com 1ª e 2ª voz (MP3/WAV) e vídeo com letra. Cada música sai por R$ 15,00, mas se você solicitar duas ou mais, o preço cai para R$ 10,00 cada. Se tiver interesse, é só chamar no [email protected]! 👍" * **Exemplo de Resposta PADRÃO para pedido de UMA música nova (seguindo 4.2 RIGOROSAMENTE):** "Olá [NomeDoUsuario], obrigado pela sugestão da música [Nome da Música]! Podemos fazer essa música pra você sob encomenda sim. Entregamos o playback com 1ª e 2ª voz (MP3/WAV) e vídeo com letra. Cada música sai por R$ 15,00, mas se você solicitar duas ou mais, o preço cai para R$ 10,00 cada. Se tiver interesse, é só chamar no [email protected]! 👍" 5. **Comentários Críticos:** Agradeça o feedback brevemente, sem prometer correções. 6. **Restrição Crucial:** Não dê a entender que você oferece playbacks gratuitamente. `; function log(message, ...optionalParams) { console.log("[YT Gemini Responder]", message, ...optionalParams); } function isStudioPage() { return window.location.hostname === 'studio.youtube.com'; } async function callGeminiAPI(commentText, authorName, videoTitle) { let prompt; const authorDisplayName = (authorName && authorName.toLowerCase() !== 'usuário') ? authorName : "você"; const videoContextInstruction = videoTitle && videoTitle !== "Vídeo não identificado" ? `O comentário foi feito no vídeo intitulado "${videoTitle}". Use este título para entender o tipo de conteúdo do vídeo (ex: "Acapella", "Playback", "Cover"). Ao responder a elogios sobre o vídeo, use a terminologia correta baseada no título (ex: "Que bom que gostou da nossa acapella!" se o título contiver "Acapella", ou "Obrigado por curtir nosso playback!" se o título contiver "Playback").` : "O contexto do vídeo específico não foi identificado. Responda de forma mais genérica ou focada apenas no texto do comentário."; if (!commentText || commentText.trim() === '' || commentText === "[Comentário vazio ou apenas com emoji/caracteres especiais]") { prompt = `${PERSONA_CONTEXT}\n\n${videoContextInstruction}\n\nO usuário "${authorName}" deixou um comentário que parece vazio ou contém apenas emojis/caracteres especiais. Responda de forma amigável como Play da Segunda, mencionando o nome "${authorName}" se ele for específico (ex: "Olá ${authorName}! Tudo bem?"). Se o nome for genérico, apenas uma saudação amigável.`; log("Enviando prompt para comentário vazio para Gemini, autor:", authorName, "vídeo:", videoTitle); } else { const userMentionInstruction = (authorName && authorName.toLowerCase() !== 'usuário') ? `Comece sua resposta mencionando o nome do usuário, "${authorName}" (por exemplo, "Olá ${authorName}, ...").` : "Comece sua resposta de forma amigável."; prompt = `${PERSONA_CONTEXT}\n\n${videoContextInstruction}\n\nResponda como Play da Segunda ao seguinte comentário feito pelo usuário "${authorName}":\n\nComentário: "${commentText}"\n\nSua resposta para ${authorDisplayName} (máximo 2-3 frases, no estilo do Play da Segunda, no mesmo idioma do comentário original. ${userMentionInstruction}):`; log(`Enviando prompt para Gemini (autor: ${authorName}, vídeo: ${videoTitle}, início): ${prompt.substring(0, 500)}...`); } const requestBody = { contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: { temperature: 0.4, // REDUZIDO PARA MAIOR LITERALIDADE responseMimeType: "text/plain" }, }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: GEMINI_API_URL, headers: { "Content-Type": "application/json" }, data: JSON.stringify(requestBody), onload: function(response) { try { if (response.status >= 200 && response.status < 300) { const data = JSON.parse(response.responseText); log("Resposta da API Gemini:", data); if (data.candidates && data.candidates.length > 0 && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts.length > 0) { if (data.candidates[0].finishReason === "SAFETY" || data.candidates[0].finishReason === "RECITATION") { let blockMessage = "Resposta bloqueada por filtros de segurança da API."; if (data.candidates[0].safetyRatings) { blockMessage += " Categorias: " + data.candidates[0].safetyRatings.map(r => r.category + ":" + r.probability).join(", "); } log(blockMessage, data.candidates[0]); reject(new Error(blockMessage)); return; } resolve(data.candidates[0].content.parts[0].text.trim()); } else if (data.promptFeedback && data.promptFeedback.blockReason) { let blockMessage = "Prompt bloqueado por filtros de segurança da API."; if (data.promptFeedback.safetyRatings) { blockMessage += " Categorias: " + data.promptFeedback.safetyRatings.map(r => r.category + ":" + r.probability).join(", "); } log(blockMessage, data.promptFeedback); reject(new Error(blockMessage)); } else { log("Resposta da API não contém texto gerado ou estrutura inesperada.", data); reject(new Error("Resposta da API não contém texto gerado.")); } } else { const errorData = response.responseText ? JSON.parse(response.responseText) : { error: { message: response.statusText } }; log(`Erro da API Gemini: ${response.status} - ${errorData.error?.message || response.statusText}`, errorData); reject(new Error(`Erro da API Gemini: ${errorData.error?.message || response.statusText}`)); } } catch (e) { log(`Erro ao processar resposta da API: ${e.message}`, response.responseText); reject(new Error(`Erro ao processar resposta da API: ${e.message}`)); } }, onerror: function(error) { log(`Erro de rede ou GM_xmlhttpRequest: ${error.statusText || error.message || 'Erro desconhecido'}`, error); reject(new Error(`Erro de rede ou GM_xmlhttpRequest: ${error.statusText || error.message || 'Erro desconhecido'}`)); } }); }); } async function handleGeminiButtonClick(event) { const geminiButton = event.currentTarget; geminiButton.textContent = 'Gerando...'; geminiButton.disabled = true; const onStudioPage = isStudioPage(); let commentElement, commentTextElement, originalCommentText, actionButtonsBar, nativeReplyButtonElement, replyDialogContainer, replyTextArea; let replyDialogRenderer; let authorNameElement, authorName = "usuário"; let videoTitle = "Vídeo não identificado"; let mainCommentNode; if (onStudioPage) { commentElement = geminiButton.closest('ytcp-comment-thread') || geminiButton.closest('ytcp-comment'); if (!commentElement) { log("Studio: Não foi possível encontrar o elemento do comentário pai (ytcp-comment-thread/ytcp-comment)."); geminiButton.textContent = 'Erro: Comentário?'; geminiButton.disabled = false; return; } mainCommentNode = commentElement.querySelector('ytcp-comment') || commentElement; authorNameElement = mainCommentNode.querySelector('div#metadata a#name yt-formatted-string.author-text'); commentTextElement = mainCommentNode.querySelector('yt-formatted-string#content-text'); actionButtonsBar = mainCommentNode.querySelector('ytcp-comment-action-buttons#action-buttons'); nativeReplyButtonElement = actionButtonsBar?.querySelector('ytcp-comment-button#reply-button button[aria-label="Responder"]'); const videoThumbnailElement = mainCommentNode.querySelector('ytcp-comment-video-thumbnail#video-thumbnail'); if (videoThumbnailElement) { const videoTitleElement = videoThumbnailElement.querySelector('div#video-title yt-formatted-string'); if (videoTitleElement) { videoTitle = videoTitleElement.textContent.trim(); log(`Studio: Título do vídeo extraído: ${videoTitle}`); } else { log("Studio: Elemento do título do vídeo (yt-formatted-string) não encontrado dentro de ytcp-comment-video-thumbnail."); } } else { log("Studio: Elemento ytcp-comment-video-thumbnail não encontrado."); const studioVideoTitleEditor = document.querySelector('ytcp-video-metadata-editor #title-textarea #text-input'); if (studioVideoTitleEditor) { videoTitle = studioVideoTitleEditor.textContent.trim(); log(`Studio: Título do vídeo (fallback da página de edição) extraído: ${videoTitle}`); } } } else { // youtube.com commentElement = geminiButton.closest('ytd-comment-thread-renderer'); if (!commentElement) { commentElement = geminiButton.closest('ytd-comment-view-model'); } if (!commentElement) { log("YT: Não foi possível encontrar o elemento do comentário pai (ytd-comment-thread-renderer / ytd-comment-view-model)."); geminiButton.textContent = 'Erro: Comentário?'; geminiButton.disabled = false; return; } mainCommentNode = commentElement.querySelector('ytd-comment-renderer') || commentElement.querySelector('ytd-comment-view-model'); if (mainCommentNode) { authorNameElement = mainCommentNode.querySelector('#author-text'); commentTextElement = mainCommentNode.querySelector('yt-attributed-string#content-text'); actionButtonsBar = mainCommentNode.querySelector('ytd-comment-engagement-bar#action-buttons'); } else { log("YT: Não foi possível encontrar o nó principal do comentário (ytd-comment-renderer/view-model)."); geminiButton.textContent = 'Erro: Nó?'; geminiButton.disabled = false; return; } nativeReplyButtonElement = actionButtonsBar?.querySelector('ytd-button-renderer#reply-button-end button[aria-label="Responder"], ytd-button-renderer#reply-button-end button'); if (window.location.pathname.includes('/shorts/')) { const shortTitleElement = document.querySelector('span#title.ytd-reel-player-header-renderer, #reel-player-header-title'); if (shortTitleElement) { videoTitle = shortTitleElement.textContent.trim(); log(`YT (Shorts): Título do vídeo extraído: ${videoTitle}`); } else { log("YT (Shorts): Não foi possível encontrar o título do Short."); } } else if (window.location.pathname.includes('/watch')) { const titleSelectorsYT = [ 'ytd-watch-metadata #title h1 yt-formatted-string', 'yt-formatted-string.title.ytd-video-primary-info-renderer', 'h1.title yt-formatted-string.ytd-video-primary-info-renderer', 'ytd-video-primary-info-renderer h1 yt-formatted-string', '#info-contents .title yt-formatted-string' ]; for (const selector of titleSelectorsYT) { const el = document.querySelector(selector); if (el) { videoTitle = el.textContent.trim(); log(`YT (Vídeo): Título do vídeo extraído com seletor "${selector}": ${videoTitle}`); break; } } if (videoTitle === "Vídeo não identificado") { log("YT (Vídeo): Não foi possível encontrar o título do vídeo na página com os seletores conhecidos."); } } else { log("YT: Página não é de vídeo ou shorts, título do vídeo não será buscado ativamente."); } } if (authorNameElement) { authorName = authorNameElement.textContent.trim(); log(`Nome do autor extraído: ${authorName}`); } else { log(`${onStudioPage ? "Studio" : "YT"}: Não foi possível encontrar o nome do autor. Usando fallback "usuário".`); } if (!commentTextElement) { log(`${onStudioPage ? "Studio" : "YT"}: Não foi possível encontrar o texto do comentário.`); geminiButton.textContent = 'Erro: Texto?'; geminiButton.disabled = false; return; } originalCommentText = commentTextElement.innerText || commentTextElement.textContent; if (!originalCommentText || originalCommentText.trim() === '') { originalCommentText = "[Comentário vazio ou apenas com emoji/caracteres especiais]"; log("Comentário vazio detectado, usando texto padrão para processamento"); } try { if (!actionButtonsBar) { log(`${onStudioPage ? "Studio" : "YT"}: Não foi possível encontrar o container dos botões de ação.`); throw new Error("Container de botões de ação não encontrado."); } if (nativeReplyButtonElement) { log(`${onStudioPage ? "Studio" : "YT"}: Clicando no botão de Responder nativo:`, nativeReplyButtonElement); nativeReplyButtonElement.click(); } else { log(`${onStudioPage ? "Studio" : "YT"}: Não foi possível encontrar o botão de 'Responder' nativo.`); throw new Error("Botão 'Responder' nativo não encontrado."); } await new Promise(resolve => setTimeout(resolve, 800)); const generatedReply = await callGeminiAPI(originalCommentText, authorName, videoTitle); let processedReply = generatedReply.replace(/\s\s+/g, ' ').trim(); log("Resposta da Gemini após limpeza de espaços:", processedReply); let replyDialogElement; if (onStudioPage) { const commentNodeForReplyDialog = actionButtonsBar.closest('ytcp-comment'); replyDialogContainer = commentNodeForReplyDialog?.querySelector('div#reply-dialog-container'); replyDialogElement = replyDialogContainer?.querySelector('ytcp-commentbox#reply-dialog-id'); replyTextArea = replyDialogElement?.querySelector('textarea#textarea'); } else { let replyDialogParent = actionButtonsBar.closest('ytd-comment-renderer') || actionButtonsBar.closest('ytd-comment-view-model'); replyDialogRenderer = replyDialogParent?.querySelector('ytd-comment-reply-dialog-renderer'); if (replyDialogRenderer) { replyDialogElement = replyDialogRenderer; replyTextArea = replyDialogRenderer.querySelector('yt-formatted-string#contenteditable-textarea #contenteditable-root, div#contenteditable-root[aria-label="Adicione uma resposta…"], div#contenteditable-root[aria-label="Add a reply…"]'); } else { const simpleBox = replyDialogParent?.querySelector('ytd-comment-simplebox-renderer'); replyDialogElement = simpleBox; replyTextArea = simpleBox?.querySelector('#textarea #contenteditable-root, #textarea div#placeholder-area + div#contenteditable-root'); } } if (replyTextArea && replyDialogElement) { log(`${onStudioPage ? "Studio" : "YT"}: Área de texto para resposta encontrada:`, replyTextArea); replyTextArea.focus(); if (onStudioPage) { replyTextArea.value = ''; replyTextArea.value = processedReply; replyTextArea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); replyTextArea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); } else { replyTextArea.innerText = ''; const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', processedReply); replyTextArea.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true })); if (replyTextArea.innerText !== processedReply) { replyTextArea.innerText = processedReply; } replyTextArea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } geminiButton.textContent = 'Play da Segunda'; try { await new Promise(resolve => setTimeout(resolve, 300)); let likeButtonElement, heartButtonElement; if (onStudioPage) { const likeButtonContainer = actionButtonsBar.querySelector('ytcp-comment-toggle-button#like-button'); if (likeButtonContainer) { likeButtonElement = likeButtonContainer.querySelector('ytcp-icon-button'); if (likeButtonElement && likeButtonElement.getAttribute('aria-pressed') !== 'true') { log("Studio: Clicando no botão Gostei (original).", likeButtonElement); likeButtonElement.click(); await new Promise(resolve => setTimeout(resolve, 250)); } } const heartButtonContainer = actionButtonsBar.querySelector('ytcp-comment-creator-heart#creator-heart'); if (heartButtonContainer) { heartButtonElement = heartButtonContainer.querySelector('ytcp-icon-button'); if (heartButtonElement && heartButtonElement.getAttribute('aria-pressed') !== 'true') { log("Studio: Clicando no botão Amei (original).", heartButtonElement); heartButtonElement.click(); await new Promise(resolve => setTimeout(resolve, 250)); } } } else { const likeButtonContainerYT = actionButtonsBar.querySelector('#like-button'); if (likeButtonContainerYT) { likeButtonElement = likeButtonContainerYT.querySelector('button'); if (likeButtonElement && likeButtonElement.getAttribute('aria-pressed') !== 'true') { log("YT: Clicando no botão Gostei (original).", likeButtonElement); likeButtonElement.click(); await new Promise(resolve => setTimeout(resolve, 250)); } } const heartButtonContainerYT = actionButtonsBar.querySelector('#creator-heart'); if (heartButtonContainerYT) { heartButtonElement = heartButtonContainerYT.querySelector('button'); if (heartButtonElement && heartButtonElement.getAttribute('aria-pressed') !== 'true') { log("YT: Clicando no botão Amei (original).", heartButtonElement); heartButtonElement.click(); await new Promise(resolve => setTimeout(resolve, 250)); } } } } catch (e) { log("Erro ao tentar clicar em Gostei/Amei (original):", e.message); } await new Promise(resolve => setTimeout(resolve, 500)); let submitButton; if (onStudioPage) { submitButton = replyDialogElement.querySelector('ytcp-comment-button#submit-button button'); } else { submitButton = replyDialogElement.querySelector('#submit-button button, #reply-button button'); } if (submitButton && !submitButton.disabled) { log(`${onStudioPage ? "Studio" : "YT"}: Clicando no botão de submit da resposta.`, submitButton); submitButton.click(); await new Promise(resolve => setTimeout(resolve, 1500)); try { let ownReplyElement; let attempts = 0; const maxAttempts = 10; while (!ownReplyElement && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 1000)); attempts++; log(`${onStudioPage ? "Studio" : "YT"}: Tentativa ${attempts} de encontrar a própria resposta...`); if (onStudioPage) { const repliesContainer = mainCommentNode.closest('ytcp-comment-thread')?.querySelector('ytcp-comment-replies#replies'); if (repliesContainer) { const replies = repliesContainer.querySelectorAll('ytcp-comment.expanded-replies'); for (const reply of replies) { const authorBadge = reply.querySelector('ytcp-author-comment-badge[is-creator]'); const authorNameEl = reply.querySelector('a#name yt-formatted-string.author-text, ytcp-author-comment-badge a#badge-name yt-formatted-string'); if (authorBadge || (authorNameEl && authorNameEl.textContent.trim() === YOUR_CHANNEL_NAME_FOR_REPLY_LIKE)) { const replyText = reply.querySelector('yt-formatted-string#content-text')?.textContent.trim(); if (replyText && processedReply.includes(replyText.substring(0, Math.min(replyText.length, 30) ))) { ownReplyElement = reply; break; } } } } } else { const repliesContainer = mainCommentNode.querySelector('#replies'); if (repliesContainer) { const replies = repliesContainer.querySelectorAll('ytd-comment-renderer.ytd-comment-replies-renderer, ytd-comment-view-model.ytd-comment-replies-renderer'); for (const reply of replies) { const authorNameEl = reply.querySelector('#author-text'); if (authorNameEl && authorNameEl.textContent.trim() === YOUR_CHANNEL_NAME_FOR_REPLY_LIKE) { const replyText = reply.querySelector('yt-attributed-string#content-text')?.textContent.trim(); if (replyText && processedReply.includes(replyText.substring(0, Math.min(replyText.length, 30) ))) { ownReplyElement = reply; break; } } } } } } if (ownReplyElement) { log(`${onStudioPage ? "Studio" : "YT"}: Própria resposta encontrada.`, ownReplyElement); const ownReplyActionButtons = onStudioPage ? ownReplyElement.querySelector('ytcp-comment-action-buttons#action-buttons') : ownReplyElement.querySelector('ytd-comment-engagement-bar#action-buttons'); if (ownReplyActionButtons) { const ownReplyLikeButtonContainer = onStudioPage ? ownReplyActionButtons.querySelector('ytcp-comment-toggle-button#like-button') : ownReplyActionButtons.querySelector('#like-button'); if (ownReplyLikeButtonContainer) { const ownReplyLikeButton = onStudioPage ? ownReplyLikeButtonContainer.querySelector('ytcp-icon-button') : ownReplyLikeButtonContainer.querySelector('button'); if (ownReplyLikeButton && ownReplyLikeButton.getAttribute('aria-pressed') !== 'true') { log(`${onStudioPage ? "Studio" : "YT"}: Clicando no botão Gostei da própria resposta.`, ownReplyLikeButton); ownReplyLikeButton.click(); } else if (ownReplyLikeButton) { log(`${onStudioPage ? "Studio" : "YT"}: Botão Gostei da própria resposta já está ativo ou não encontrado corretamente.`); } else { log(`${onStudioPage ? "Studio" : "YT"}: Botão Gostei (interno) da própria resposta não encontrado.`); } } else { log(`${onStudioPage ? "Studio" : "YT"}: Container do botão Gostei da própria resposta não encontrado.`); } } else { log(`${onStudioPage ? "Studio" : "YT"}: Barra de ações da própria resposta não encontrada.`); } } else { log(`${onStudioPage ? "Studio" : "YT"}: Não foi possível encontrar a própria resposta para dar like após ${maxAttempts} tentativas.`); } } catch (e) { log("Erro ao tentar dar like na própria resposta:", e.message, e.stack); } } else { log(`${onStudioPage ? "Studio" : "YT"}: Botão de submit da resposta não encontrado ou desabilitado.`); throw new Error("Botão de submit da resposta não encontrado ou desabilitado."); } } else { log(`${onStudioPage ? "Studio" : "YT"}: Não foi possível encontrar a área de texto ou o diálogo de resposta.`); throw new Error("Caixa de texto ou diálogo de resposta não encontrado."); } } catch (error) { log(`Erro ao gerar ou inserir resposta: ${error.message}`, error.stack); geminiButton.textContent = 'Erro API/UI'; alert(`Erro Gemini/UI: ${error.message}\nVerifique o console para mais detalhes (Ctrl+Shift+J).`); } finally { geminiButton.disabled = false; } } function addGeminiButton(actionButtonsContainer) { if (actionButtonsContainer.querySelector(`.${GEMINI_BUTTON_CLASS}`)) { return; } const onStudioPage = isStudioPage(); const nativeReplyButtonProvider = onStudioPage ? actionButtonsContainer.querySelector('ytcp-comment-button#reply-button') : actionButtonsContainer.querySelector('ytd-button-renderer#reply-button-end'); if (!nativeReplyButtonProvider) { return; } const geminiButton = document.createElement('button'); geminiButton.textContent = 'Play da Segunda'; geminiButton.classList.add(GEMINI_BUTTON_CLASS); geminiButton.type = 'button'; Object.assign(geminiButton.style, { marginLeft: '8px', padding: 'var(--yt-button-padding, var(--ytcp-button-padding, 8px 16px))', border: 'none', borderRadius: '18px', backgroundColor: 'var(--yt-spec-brand-button-background, var(--ytcp-call-to-action, #065fd4))', color: 'var(--yt-spec-static-brand-white, var(--ytcp-call-to-action-color, white))', fontSize: 'var(--ytd-button-label-size, var(--ytcp-font-subheading_-_font-size, 1.4rem))', fontWeight: 'var(--ytd-button-label-weight, var(--ytcp-font-subheading_-_font-weight, 500))', cursor: 'pointer', lineHeight: 'var(--ytd-button-label-line-height, var(--ytcp-font-subheading_-_line-height, 2rem))' }); geminiButton.addEventListener('mouseenter', () => geminiButton.style.backgroundColor = 'var(--yt-spec-brand-button-background-hover, var(--ytcp-call-to-action-hover, #0552b5))'); geminiButton.addEventListener('mouseleave', () => geminiButton.style.backgroundColor = 'var(--yt-spec-brand-button-background, var(--ytcp-call-to-action, #065fd4))'); geminiButton.addEventListener('click', handleGeminiButtonClick); nativeReplyButtonProvider.parentNode.insertBefore(geminiButton, nativeReplyButtonProvider.nextSibling); } function scanForCommentActions() { const onStudioPage = isStudioPage(); const selector = onStudioPage ? 'ytcp-comment ytcp-comment-action-buttons#action-buttons' : 'ytd-comment-renderer ytd-comment-engagement-bar#action-buttons, ytd-comment-view-model ytd-comment-engagement-bar#action-buttons'; const actionBars = document.querySelectorAll(selector); actionBars.forEach(bar => { const hasReplyCapability = onStudioPage ? bar.querySelector('ytcp-comment-button#reply-button button[aria-label="Responder"]') : bar.querySelector('ytd-button-renderer#reply-button-end button[aria-label="Responder"], ytd-button-renderer#reply-button-end button'); if (hasReplyCapability) { addGeminiButton(bar); } }); } const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { let needsScan = false; mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const onStudioPage = isStudioPage(); const mainCommentContainerSelector = onStudioPage ? 'ytcp-comment-thread, ytcp-comment-view-model, ytcp-comment-simplebox-renderer, ytcp-comment-dialog, ytcp-comment' : 'ytd-comments#comments, ytd-comment-thread-renderer, ytd-comment-view-model, ytd-comment-simplebox-renderer, ytd-comment-dialog, ytd-comment-renderer'; const actionBarSelector = onStudioPage ? 'ytcp-comment-action-buttons#action-buttons' : 'ytd-comment-engagement-bar#action-buttons'; if (node.matches && (node.matches(mainCommentContainerSelector) || node.querySelector(`${mainCommentContainerSelector}, ${actionBarSelector}`))) { needsScan = true; } else if (node.querySelector && (node.querySelector(mainCommentContainerSelector) || node.querySelector(actionBarSelector))) { needsScan = true; } } }); if (needsScan) { scanForCommentActions(); } } } }); log("Iniciando YouTube Gemini Comment Responder v1.7.4 como 'Play da Segunda'."); setTimeout(() => { scanForCommentActions(); observer.observe(document.body, { childList: true, subtree: true }); log("Observador de mutações iniciado."); }, 3000); })();
Segurança & Configuração
DEBUG=true
) com logs detalhados via console.debug
.Motor de “Inteligência × 100”
classifyScenario
) decide, antes da chamada ao Gemini, em qual de cinco cenários o comentário se encaixa:COMPLIMENT
, REQUEST_PLAYBACK
, CRITICISM
, QUESTION
, OTHER
.temperature
(0 – 0.7)REQUEST_PLAYBACK
).Prompt Engineering 2.0
system
(persona + regras) e role user
(comentário) para dar à Gemini maior coerência.Robustez de UI
requestAnimationFrame
.autoReact
.alert
só se DEBUG
, evitando pop-ups a cada erro.Qualidade de código
Pronto para copiar/colar no Tampermonkey:
javascript// ==UserScript== // @name YouTube Gemini Comment Responder - Play da Segunda (v2.0.0) // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description Responde comentários do YouTube como "Play da Segunda" usando Gemini, agora 100× mais inteligente e modular. // @author Seu Nome Aqui // @match https://www.youtube.com/* // @match https://studio.youtube.com/* // @grant GM_xmlhttpRequest // @connect generativelanguage.googleapis.com // @license MIT // ==/UserScript== // ------------------------------------------------------------- (function () { 'use strict'; /* ---------- CONFIG ---------- */ const DEBUG = false; // mude para true para logs detalhados const MODEL_NAME = 'gemini-1.5-flash-latest'; const API_BASE = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL_NAME}:generateContent?key=`; const BTN_CLASS = 'gemini-reply-button-yt'; const CHANNEL_HANDLE = '@playdasegunda'; // ----- Persona & Prompt ----- const PERSONA = ` Você é o "Play da Segunda", canal musical que produz playbacks (1ª/2ª voz) e acapellas. Estilo: educado, direto, informal, simples, 🔥 e breve (≤ 3 frases). Sempre que possível mencione o nome do usuário no início ("Olá Fulano, ..."). Regra de OURO (venda de playback): * Termine exatamente com: "Cada música sai por R$ 15,00, mas se você solicitar duas ou mais, o preço cai para R$ 10,00 cada. Se tiver interesse, é só chamar no [email protected]!" * Nunca altere essa frase nem oculte o preço. Nunca prometa conteúdo gratuito. Use emojis de forma moderada. Língua: responda no mesmo idioma do comentário.`.trim(); /* ---------- Utils ---------- */ const log = (...a) => DEBUG && console.debug('[Play2ª]', ...a); const isStudio = () => location.hostname === 'studio.youtube.com'; const getApiKey = () => { let key = localStorage.getItem('PLAY2_API_KEY'); if (!key) { key = prompt('Insira sua API-Key Gemini (será salva localmente):'); if (key) localStorage.setItem('PLAY2_API_KEY', key.trim()); } return key; }; /* ---------- NLP helpers (leves) ---------- */ const classifyScenario = (text) => { const t = text.toLowerCase(); const isRequest = /(faz|poderia|teria|completa|vers(ã)o completa|playback|segunda voz|primeira voz|tom|quero|instrumental|play back)/.test( t, ); const isCompliment = /(obrigad|amei|gostei|top|perfeito|show|maravilhoso|lind[oa])/.test(t); const isCriticism = /(ruim|horr�vel|não gostei|péssimo|horrível|errado)/.test(t); const isQuestion = /\?$/.test(t) && !isRequest; if (isRequest) return 'REQUEST_PLAYBACK'; if (isCompliment) return 'COMPLIMENT'; if (isCriticism) return 'CRITICISM'; if (isQuestion) return 'QUESTION'; return 'OTHER'; }; const sentimentEmoji = (text) => { if (/(ruim|horrível|péssimo|triste|chato)/i.test(text)) return '😔'; if (/(obrigad|amei|gostei|maravilhoso|show)/i.test(text)) return '😊'; return '😉'; }; /* ---------- Gemini call ---------- */ async function generateReply(comment, authorName, videoTitle) { const scenario = classifyScenario(comment); log('Scenario:', scenario); const temperatureMap = { COMPLIMENT: 0.3, REQUEST_PLAYBACK: 0.4, CRITICISM: 0.4, QUESTION: 0.5, OTHER: 0.4, }; const temperature = temperatureMap[scenario] || 0.4; const extraRules = scenario === 'REQUEST_PLAYBACK' ? 'Lembre-se de seguir a Regra de OURO obrigatoriamente.' : 'Não mencione preços nem e-mails, exceto se for pedido explícito de playback.'; const sysPrompt = `${PERSONA}\n${extraRules}`; const userPrompt = ` Comentário do usuário "${authorName || 'usuário'}" no vídeo "${videoTitle}": "${comment}" Tarefa: responda como Play da Segunda (${scenario}). Inclua o emoji ${sentimentEmoji( comment, )} se apropriado. `.trim(); const body = { contents: [ { role: 'system', parts: [{ text: sysPrompt }] }, { role: 'user', parts: [{ text: userPrompt }] }, ], generationConfig: { temperature, responseMimeType: 'text/plain', }, }; const apiKey = getApiKey(); if (!apiKey) throw new Error('API-Key ausente.'); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: `${API_BASE}${apiKey}`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(body), onload: (res) => { try { if (res.status >= 200 && res.status < 300) { const data = JSON.parse(res.responseText); const text = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim(); if (text) return resolve(text); return reject(new Error('Resposta vazia da API.')); } const msg = JSON.parse(res.responseText)?.error?.message || res.statusText; reject(new Error(msg)); } catch (e) { reject(new Error('Falha ao processar resposta da API.')); } }, onerror: () => reject(new Error('Falha de rede na requisição.')), }); }); } /* ---------- DOM helpers ---------- */ function findActionBar(root) { return root.querySelector( isStudio() ? 'ytcp-comment-action-buttons#action-buttons' : 'ytd-comment-engagement-bar#action-buttons', ); } function addButton(actionBar) { if (actionBar.querySelector(`.${BTN_CLASS}`)) return; const nativeReply = actionBar.querySelector('button[aria-label*=Responder]'); if (!nativeReply) return; const btn = document.createElement('button'); btn.className = BTN_CLASS; btn.textContent = 'Play da Segunda'; Object.assign(btn.style, { marginLeft: '8px', padding: '6px 12px', border: 'none', borderRadius: '18px', background: '#065fd4', color: '#fff', cursor: 'pointer', fontWeight: 500, }); btn.onclick = () => handleClick(btn); nativeReply.after(btn); } async function handleClick(btn) { btn.disabled = true; btn.textContent = 'Gerando…'; try { const commentNode = btn.closest( isStudio() ? 'ytcp-comment' : 'ytd-comment-renderer,ytd-comment-view-model', ); const commentText = commentNode.querySelector( isStudio() ? 'yt-formatted-string#content-text' : 'yt-attributed-string#content-text', )?.innerText.trim(); const authorName = commentNode .querySelector('#author-text, a#name yt-formatted-string')?.innerText.trim(); const videoTitle = document.querySelector('h1.title yt-formatted-string, #title h1')?.innerText.trim() || 'Vídeo não identificado'; const reply = await generateReply(commentText, authorName, videoTitle); log('Gemini reply:', reply); /* abrir caixa de resposta nativa */ const replyBtn = commentNode.querySelector('button[aria-label*=Responder]'); replyBtn?.click(); await new Promise((r) => setTimeout(r, 500)); const textarea = commentNode.querySelector( isStudio() ? 'textarea#textarea' : 'div#contenteditable-root[aria-label*="Adicione"],div#contenteditable-root[aria-label*="Add"]', ); textarea.focus(); if (isStudio()) { textarea.value = reply; textarea.dispatchEvent(new Event('input', { bubbles: true })); } else { textarea.innerText = reply; textarea.dispatchEvent(new Event('input', { bubbles: true })); } const submit = commentNode.querySelector( isStudio() ? 'ytcp-comment-button#submit-button button' : '#submit-button button', ); submit?.click(); /* auto-like/heart */ autoReact(commentNode); btn.textContent = 'Play da Segunda ✓'; } catch (err) { console.error(err); alert(`Erro: ${err.message}`); btn.textContent = 'Erro'; } finally { btn.disabled = false; setTimeout(() => (btn.textContent = 'Play da Segunda'), 3000); } } function autoReact(node) { setTimeout(() => { const like = node.querySelector('button[aria-pressed="false"][aria-label*="Gostei"],button[aria-pressed="false"][aria-label*="Like"]'); like?.click(); const heart = node.querySelector('button[aria-label*="Amei"],button[aria-label*="Love"]'); heart?.click(); }, 1800); } /* ---------- Observer ---------- */ const scan = () => document .querySelectorAll( isStudio() ? 'ytcp-comment-action-buttons#action-buttons' : 'ytd-comment-engagement-bar#action-buttons', ) .forEach(addButton); const obs = new MutationObserver((m) => { if (m.some((x) => x.addedNodes.length)) scan(); }); log('Play da Segunda v2.0 carregado.'); setTimeout(() => { scan(); obs.observe(document.body, { childList: true, subtree: true }); }, 2500); })();
Como usar
Pronto! Persona turbinada, respostas mais contextuais e zero dor de cabeça. 🎸