בקשה | אני רוצה להגדיר שכל קובץ וידאו / אודיו שמגיע אלי למייל יקבל בחזרה תמלול לטקסט, זה בינתיים הסקריפט:
עזרה הדדית - מחשבים וטכנולוגיה
1
פוסטים
1
כותבים
10
צפיות
1
עוקבים
-
הבנתי שיש דרך לעשות את זה ללא צורך בסקריפט, אבל זה מה שהצלחתי בס"ד לכתוב בינתיים - סקריפט שמשתמש בAPI של מודלים בשם Deepgram וGroq (שניהם באיכות בינונית בעברית) ובעיקרון גם של Gemini - רק שהוא מחזיר שגיאה 403 מסיבה לא ברורה (לי).
אשמח לכל הצעת שיפור:// --- Deepgram --- const DEEPGRAM_API_KEYS = [ כאן מופיע מפתח API של Deepgram ]; const DEEPGRAM_MODEL = "nova-3"; // --- Groq --- const GROQ_API_KEY = כאן מופיע מפתח API של Groq"; // --- Gemini --- const GEMINI_API_KEYS = [ כאן מופיע מפתח API של Gemini ]; const GEMINI_MODEL = "gemini-3.5-flash"; const MAX_THREADS_PER_RUN = 8; const PROCESSED_LABEL_NAME = "Transcribed"; const MAX_RETRIES = 3; const DEFAULT_TRANSCRIPTION_LANGUAGE = "he"; const ENGLISH_SUBJECT_KEYWORDS = ["אנגלית", "באנגלית", "english"]; function detectLanguageFromSubject_(subject) { const normalizedSubject = (subject || "").toLowerCase(); const isEnglish = ENGLISH_SUBJECT_KEYWORDS.some(keyword => normalizedSubject.includes(keyword.toLowerCase())); return isEnglish ? "en" : DEFAULT_TRANSCRIPTION_LANGUAGE; } const TRANSCRIPTION_PROMPTS = { he: "תמלול שיחה בעברית, כולל סימני פיסוק, פסיקים ונקודות במקומות המתאימים.", en: "Transcription of a conversation in English, including proper punctuation, commas and periods." }; const GEMINI_TRANSCRIPTION_PROMPTS = { he: "תמלל את קובץ האודיו המצורף במדויק לעברית, כולל סימני פיסוק, פסיקים ונקודות במקומות המתאימים. החזר רק את הטקסט המתומלל, ללא הערות נוספות.", en: "Transcribe the attached audio file precisely in English, including proper punctuation, commas and periods. Return only the transcribed text, with no extra comments." }; const USE_LLM_POST_PROCESSING = true; const GROQ_LLM_MODEL = "llama-3.3-70b-versatile"; const POST_PROCESSING_SYSTEM_PROMPTS = { he: "אתה עוזר שמתקן תמלולים בעברית. תקן שגיאות פיסוק, רווחים ומונחים ברורים, " + "אך אל תשנה את המשמעות או תוסיף/תגרע תוכן. החזר רק את הטקסט המתוקן, ללא הערות נוספות.", en: "You are an assistant that fixes transcripts in English. Correct punctuation, spacing and " + "obvious misheard words, but do not change the meaning or add/remove content. Return only the corrected text, with no extra comments." }; // ========================= לוגיקה ראשית ========================= function transcribeIncomingEmailsWithGroq() { const processedLabel = getOrCreateLabel_(PROCESSED_LABEL_NAME); // מיילים עם קובץ מצורף בפועל, או כל מייל שמכיל קישור ל-Drive (בין אם התראת שיתוף רשמית ובין אם קישור ששלח מישהו ידנית) const searchQuery = `is:unread (filename:mp3 OR filename:wav OR filename:m4a OR filename:mp4 OR filename:mov OR "drive.google.com") -label:${PROCESSED_LABEL_NAME}`; const threads = GmailApp.search(searchQuery, 0, MAX_THREADS_PER_RUN); if (threads.length === 0) { Logger.log("No new emails with audio/video found."); return; } let successCount = 0; let errorCount = 0; for (const thread of threads) { try { processThread_(thread, processedLabel); successCount++; } catch (e) { errorCount++; Logger.log(`Failed to process thread "${thread.getFirstMessageSubject()}": ${e.toString()}`); thread.addLabel(processedLabel); } } Logger.log(`Run complete. Success: ${successCount}, Errors: ${errorCount}`); } function processThread_(thread, processedLabel) { const messages = thread.getMessages(); let transcriptionText = ""; let processedAny = false; for (const message of messages) { if (!message.isUnread()) continue; const attachments = message.getAttachments(); const driveBlobs = getDriveBlobsFromMessage_(message); const audioItems = [...attachments, ...driveBlobs]; const subject = message.getSubject(); const language = detectLanguageFromSubject_(subject); for (const attachment of audioItems) { if (!isAudioOrVideo(attachment.getName())) continue; processedAny = true; const fileSizeMB = attachment.getBytes().length / (1024 * 1024); if (fileSizeMB > 25) { transcriptionText += `\n--- שגיאה עבור ${attachment.getName()} ---\n[הקובץ כבד מדי]\n`; continue; } Logger.log("Processing attachment: " + attachment.getName()); if (transcriptionText.length > 0) Utilities.sleep(800); // שלושה מקורות תמלול במקביל (לוגית - כל אחד עצמאי) const deepgramResult = transcribeAttachment_(attachment, language); const groqResult = sendToGroqSTT(attachment, language); const geminiResult = sendToGeminiSTT_(attachment, language); // ניקוי LLM לכל תוצאה תקינה בנפרד const deepgramClean = maybeCleanup_(deepgramResult, language); const groqClean = maybeCleanup_(groqResult, language); const geminiClean = maybeCleanup_(geminiResult, language); transcriptionText += `\n--- תמלול עבור ${attachment.getName()} (במייל: ${subject}) ---\n`; transcriptionText += `• Deepgram: ${deepgramClean || "[שגיאה]"}\n\n`; transcriptionText += `• Groq (Whisper): ${groqClean || "[שגיאה]"}\n\n`; transcriptionText += `• Gemini: ${geminiClean || "[שגיאה]"}\n`; } message.markRead(); } if (!processedAny) { thread.addLabel(processedLabel); return; } if (transcriptionText) { const replyBody = `שלום,\n\nמצורפים 3 תמלולים שונים עבור כל קובץ:\n${transcriptionText}\nבברכה,\nמערכת תמלול אוטומטית • ${new Date().toLocaleString('he-IL')}`; const lastMessage = messages[messages.length - 1]; lastMessage.reply(replyBody); } thread.addLabel(processedLabel); } function maybeCleanup_(result, language) { if (result && USE_LLM_POST_PROCESSING && !isErrorMessage_(result)) { const cleaned = cleanupTranscriptionWithLLM(result, language); if (cleaned) return cleaned; } return result; } // ========================= פונקציות (ללא שינוי) ========================= function isErrorMessage_(text) { return typeof text === "string" && text.startsWith("[שגיאה"); } function getOrCreateLabel_(name) { return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name); } function isAudioOrVideo(filename) { const ext = filename.split('.').pop().toLowerCase(); const validExtensions = ['mp3', 'wav', 'm4a', 'ogg', 'mp4', 'flac', 'webm', 'mpeg', 'mpga', 'mov', 'avi', 'mkv']; return validExtensions.includes(ext); } // --- זיהוי קבצים ששותפו דרך Google Drive (במקום לצפות לקובץ מצורף) --- function extractDriveFileIds_(html) { const ids = new Set(); const patterns = [ /drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)/g, /drive\.google\.com\/open\?id=([a-zA-Z0-9_-]+)/g, /[?&]id=([a-zA-Z0-9_-]{20,})/g ]; for (const re of patterns) { let match; while ((match = re.exec(html)) !== null) { ids.add(match[1]); } } return Array.from(ids); } function getDriveBlobsFromMessage_(message) { const blobs = []; let body; try { body = message.getBody(); // HTML - כולל את קישורי ה-Drive } catch (e) { return blobs; } const fileIds = extractDriveFileIds_(body); for (const fileId of fileIds) { try { const file = DriveApp.getFileById(fileId); const name = file.getName(); if (!isAudioOrVideo(name)) continue; // מדלגים על קבצים שאינם אודיו/וידאו (למשל, לוגו בחתימה) blobs.push(file.getBlob()); } catch (e) { Logger.log(`Could not access Drive file ${fileId}: ${e}`); } } return blobs; } const GROQ_SUPPORTED_EXTENSIONS = ['flac', 'mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'ogg', 'wav', 'webm']; function getMimeTypeForExtension_(ext) { const map = { 'flac': 'audio/flac', 'mp3': 'audio/mp3', 'mp4': 'video/mp4', 'mpeg': 'audio/mpeg', 'mpga': 'audio/mpeg', 'm4a': 'audio/mp4', 'ogg': 'audio/ogg', 'wav': 'audio/wav', 'webm': 'audio/webm' }; return map[ext] || 'audio/mp3'; } function transcribeAttachment_(blob, language) { for (let i = 0; i < DEEPGRAM_API_KEYS.length; i++) { const apiKey = DEEPGRAM_API_KEYS[i]; const result = sendToDeepgramWithKey_(blob, language, apiKey); if (result.status === "ok") return result.transcript; if (result.status === "no_credits") continue; } Logger.log("All Deepgram keys failed."); return null; } function sendToDeepgramWithKey_(blob, language, apiKey) { const originalName = blob.getName() || "audio"; const ext = originalName.split('.').pop().toLowerCase(); const mimeType = getMimeTypeForExtension_(ext); const url = `https://api.deepgram.com/v1/listen?model=${DEEPGRAM_MODEL}&smart_format=true&punctuate=true&language=${language}`; const options = { method: "post", contentType: mimeType, payload: blob.getBytes(), headers: { "Authorization": "Token " + apiKey }, muteHttpExceptions: true }; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { const response = UrlFetchApp.fetch(url, options); const responseCode = response.getResponseCode(); if (responseCode === 200) { const json = JSON.parse(response.getContentText()); const transcript = json.results?.channels?.[0]?.alternatives?.[0]?.transcript || ""; return { status: "ok", transcript }; } if (responseCode === 402) return { status: "no_credits" }; if (responseCode === 429 || responseCode >= 500) { Utilities.sleep(1000 * attempt); continue; } return { status: "failed" }; } catch (e) { if (attempt === MAX_RETRIES) return { status: "failed" }; Utilities.sleep(1000 * attempt); } } return { status: "failed" }; } function sendToGroqSTT(blob, language) { const url = "https://api.groq.com/openai/v1/audio/transcriptions"; const originalName = blob.getName() || "audio"; const ext = originalName.split('.').pop().toLowerCase(); let fileBlob; if (GROQ_SUPPORTED_EXTENSIONS.includes(ext)) { const mimeType = getMimeTypeForExtension_(ext); fileBlob = Utilities.newBlob(blob.getBytes(), mimeType, originalName); } else { const fallbackName = originalName.split('.')[0] + ".mp3"; fileBlob = Utilities.newBlob(blob.getBytes(), "audio/mp3", fallbackName); } const payload = { "model": "whisper-large-v3", "file": fileBlob }; if (language) payload["language"] = language; const prompt = TRANSCRIPTION_PROMPTS[language]; if (prompt) payload["prompt"] = prompt; const options = { method: "post", headers: { "Authorization": "Bearer " + GROQ_API_KEY }, payload: payload, muteHttpExceptions: true }; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { const response = UrlFetchApp.fetch(url, options); if (response.getResponseCode() === 200) { return JSON.parse(response.getContentText()).text; } const isRetryable = response.getResponseCode() === 429 || response.getResponseCode() >= 500; if (!isRetryable || attempt === MAX_RETRIES) { let msg = `קוד שגיאה ${response.getResponseCode()}`; try { msg = JSON.parse(response.getContentText()).error.message; } catch(e){} return `[שגיאה בתמלול: ${msg}]`; } Utilities.sleep(1000 * attempt); } catch (e) { if (attempt === MAX_RETRIES) return "[שגיאה בתקשורת עם Groq]"; Utilities.sleep(1000 * attempt); } } } // --- Gemini: תמלול באמצעות שליחת האודיו כ-inline data ל-generateContent --- function sendToGeminiSTT_(blob, language) { for (let i = 0; i < GEMINI_API_KEYS.length; i++) { const apiKey = GEMINI_API_KEYS[i]; const result = sendToGeminiWithKey_(blob, language, apiKey); if (result.status === "ok") return result.transcript; if (result.status === "quota") continue; // נסה מפתח הבא if (result.status === "failed") return `[שגיאה בתמלול Gemini: ${result.errorDetail || "לא ידוע"}]`; } Logger.log("All Gemini keys failed or hit quota."); return "[שגיאה: כל מפתחות Gemini נכשלו/מוגבלים]"; } function sendToGeminiWithKey_(blob, language, apiKey) { const originalName = blob.getName() || "audio"; const ext = originalName.split('.').pop().toLowerCase(); const mimeType = getMimeTypeForExtension_(ext); const base64Audio = Utilities.base64Encode(blob.getBytes()); const promptText = GEMINI_TRANSCRIPTION_PROMPTS[language] || GEMINI_TRANSCRIPTION_PROMPTS["en"]; const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`; const payload = { contents: [ { parts: [ { text: promptText }, { inline_data: { mime_type: mimeType, data: base64Audio } } ] } ], generationConfig: { thinkingConfig: { thinkingLevel: "low" } } }; const options = { method: "post", contentType: "application/json", headers: { "x-goog-api-key": apiKey }, payload: JSON.stringify(payload), muteHttpExceptions: true }; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { const response = UrlFetchApp.fetch(url, options); const responseCode = response.getResponseCode(); if (responseCode === 200) { const json = JSON.parse(response.getContentText()); const text = json.candidates?.[0]?.content?.parts?.[0]?.text || ""; return { status: "ok", transcript: text.trim() }; } // תיעוד השגיאה האמיתית - חיוני לאבחון (401/403/429 וכו') Logger.log(`Gemini error (key ending ${apiKey.slice(-4)}, attempt ${attempt}): HTTP ${responseCode} - ${response.getContentText()}`); // 429 = חריגת קצב/מכסה. עם מפתח יחיד ננסה שוב, ואם עדיין נכשל - נעבור למפתח הבא if (responseCode === 429) { if (attempt < MAX_RETRIES) { Utilities.sleep(1000 * attempt); continue; } return { status: "quota" }; } if (responseCode >= 500) { Utilities.sleep(1000 * attempt); continue; } let errMsg = `HTTP ${responseCode}`; try { errMsg += ": " + (JSON.parse(response.getContentText()).error?.message || ""); } catch (parseErr) {} return { status: "failed", errorDetail: errMsg }; } catch (e) { if (attempt === MAX_RETRIES) return { status: "failed", errorDetail: e.toString() }; Utilities.sleep(1000 * attempt); } } return { status: "failed" }; } function cleanupTranscriptionWithLLM(rawText, language) { const url = "https://api.groq.com/openai/v1/chat/completions"; const systemPrompt = POST_PROCESSING_SYSTEM_PROMPTS[language] || POST_PROCESSING_SYSTEM_PROMPTS["en"]; const payload = { "model": GROQ_LLM_MODEL, "temperature": 0, "messages": [ { "role": "system", "content": systemPrompt }, { "role": "user", "content": rawText } ] }; const options = { method: "post", contentType: "application/json", headers: { "Authorization": "Bearer " + GROQ_API_KEY }, payload: JSON.stringify(payload), muteHttpExceptions: true }; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { const response = UrlFetchApp.fetch(url, options); if (response.getResponseCode() === 200) { const json = JSON.parse(response.getContentText()); return json.choices?.[0]?.message?.content?.trim() || null; } if (attempt === MAX_RETRIES) return null; Utilities.sleep(1000 * attempt); } catch (e) { if (attempt === MAX_RETRIES) return null; Utilities.sleep(1000 * attempt); } } }היה בסקריפט גם תמלול של שרת שבניתי דרך ngrok אבל הוא לא הספיק לתמלל ב6 דקות של הריצה אז מחקתי אותו.
שלום! נראה שהשיחה הזו מעניינת אותך, אבל עדיין אין לך חשבון.
נמאס לכם לגלול בין אותם הפוסטים בכל ביקור? כשנרשמים לחשבון, תמיד תחזרו בדיוק למקום שבו הייתם קודם, ותוכלו לבחור לקבל התראות על תגובות חדשות (בין אם במייל, ובין אם בהתראת פוש). תוכלו גם לשמור סימניות ולפרגן ב-upvote לפוסטים כדי להביע הערכה לחברי קהילה אחרים.
בעזרת התרומה שלך, הפוסט הזה יכול להיות אפילו טוב יותר 💗
הרשמה התחברות