הקלטת השיחה בלבד במודול תור
-
@CUBASE תירגמתי אותו לסקירפט גוגל עובד מעולה
משתמש באחסון של גוגל דרייב
יש בעיה שגוגל יכול לרוץ עד 6 דקות אני הגדרתי לו שכל פעם הוא סורק את כל הלוג לוקח את נתיב ההקלטה וחותך את זמן ההמתנה למענה ושיחות שהוא טיפל בהם הוא שומר בקובץ בדרייב רק שמות וככה הוא יודע לא לערוך אותם שוב אבל עדיין זה יכול לקחת זמן
אני לא יודע מה מגיע במייל אם ההקלטה אבל צריך להגדיר לו שיקח משם את נתיב ההקלטה אם הוא מופיע ואז יחפש בלוג רק את זמן ההמתנה למענה וככה כל מייל נכנס הוא יעבוד על שיחה אחת
זה הקוד// --- הגדרות מערכת --- const TOKEN = "כאן להזין טוקן"; const LOG_PATH = "ivr2:Log/LogQueueOK.2026-03-30.ymgr"; const API_URL = "https://www.call2all.co.il/ym/api/"; // --- הגדרות דרייב --- const DRIVE_FOLDER_NAME = "Yemot_Audio_Processing"; // שם התיקייה שתיווצר בדרייב const JSON_FILENAME = "processed_calls.json"; function processYmCalls() { Logger.log("--- תחילת ריצת מערכת ---"); // השגת או יצירת תיקיית העבודה בדרייב let workFolder = getOrCreateDriveFolder(DRIVE_FOLDER_NAME); // טעינת רשימת השיחות שכבר טופלו let processedPaths = getProcessedCalls(workFolder); Logger.log("שולף את קובץ הלוג: " + LOG_PATH); // --- שלב 1: קריאת הלוג מימות המשיח --- let logRes = getYmApi("RenderYMGRFile", { token: TOKEN, wath: LOG_PATH, convertType: 'json', notLoadLang: 1 }); let data = JSON.parse(logRes.getContentText()); if (!data || !data.data) { Logger.log("שגיאה: לא נמצאו נתונים בלוג או שהטוקן/נתיב שגויים."); return; } Logger.log("קובץ הלוג נקרא בהצלחה, מתחיל סריקת רשומות..."); // --- שלב 2: עיבוד הרשומות --- for (let i = 0; i < data.data.length; i++) { let row = data.data[i]; // בודק אם זו שיחה שנענתה ויש נתיב להקלטה if (row.QueueStatus === 'ANSWER' && row.QueueRecordPath) { let remotePath = row.QueueRecordPath; let waitSeconds = parseInt(row.QueueWaitingSeconds || 0); // בדיקה האם הקובץ כבר טופל if (processedPaths.indexOf(remotePath) !== -1) { continue; // מדלג } if (waitSeconds > 0) { Logger.log("זוהתה שיחה חדשה. נתיב: " + remotePath + " | זמן חיתוך: " + waitSeconds + " שניות."); // הורדת הקובץ Logger.log("מוריד את הקובץ..."); let fileResponse = getYmApi("DownloadFile", { token: TOKEN, path: remotePath }); let originalBlob = fileResponse.getBlob(); if (originalBlob.getBytes().length > 44) { originalBlob.setName("orig_" + waitSeconds + ".wav"); // שמירת הקובץ המקורי בדרייב לצורך ניתוח let origDriveFile = workFolder.createFile(originalBlob); Logger.log("הקובץ המקורי נשמר בדרייב לניתוח."); // ביצוע החיתוך Logger.log("מתחיל חיתוך..."); let trimmedBlob = smartTrimWavGas(origDriveFile.getBlob(), waitSeconds); if (trimmedBlob) { // שמירת הקובץ החתוך בדרייב let trimDriveFile = workFolder.createFile(trimmedBlob); // יצירת הנתיב החדש let pathParts = remotePath.split('/'); let fileName = pathParts.pop(); let newRemotePath = pathParts.join('/') + '/חתוכים/' + fileName; Logger.log("מעלה קובץ חדש: " + newRemotePath); // העלאה חזרה לימות המשיח let uploadRes = UrlFetchApp.fetch(API_URL + "UploadFile", { method: 'post', payload: { token: TOKEN, path: newRemotePath, file: trimDriveFile.getBlob() }, muteHttpExceptions: true }); let uploadData = JSON.parse(uploadRes.getContentText()); if (uploadData.responseStatus === 'OK') { Logger.log("הקובץ הועלה בהצלחה."); // עדכון ה-JSON processedPaths.push(remotePath); saveProcessedCalls(workFolder, processedPaths); } else { Logger.log("שגיאה בהעלאה: " + uploadRes.getContentText()); } // ניקוי הקובץ החתוך מהדרייב (העברה לאשפה) trimDriveFile.setTrashed(true); } else { Logger.log("שגיאה בחיתוך הקובץ."); } // ניקוי הקובץ המקורי מהדרייב origDriveFile.setTrashed(true); } else { Logger.log("שגיאה: הקובץ ריק או קצר מדי."); } } else { // זמן המתנה 0 - נסמן כטופל ונדלג Logger.log("שיחה בנתיב " + remotePath + " ללא זמן המתנה. מסמן כטופל ומדלג."); processedPaths.push(remotePath); saveProcessedCalls(workFolder, processedPaths); } Logger.log("--------------------------------------------------"); } } Logger.log("--- הריצה הסתיימה בהצלחה ---"); } // ================= פונקציות עזר ================= /** * פונקציה לתקשורת API בסיסית */ function getYmApi(endpoint, params) { let url = API_URL + endpoint; let options = { method: 'post', payload: params, muteHttpExceptions: true }; return UrlFetchApp.fetch(url, options); } /** * פונקציה להשגת תיקיית העבודה בדרייב או יצירתה במידה ואינה קיימת */ function getOrCreateDriveFolder(folderName) { let folders = DriveApp.getFoldersByName(folderName); if (folders.hasNext()) { return folders.next(); } else { return DriveApp.createFolder(folderName); } } /** * קריאת רשימת השיחות שטופלו מתוך ה-JSON בדרייב */ function getProcessedCalls(folder) { let files = folder.getFilesByName(JSON_FILENAME); if (files.hasNext()) { let file = files.next(); let content = file.getBlob().getDataAsString(); try { return JSON.parse(content || "[]"); } catch (e) { return []; } } return []; } /** * שמירת רשימת השיחות המעודכנת לקובץ ה-JSON בדרייב */ function saveProcessedCalls(folder, data) { let files = folder.getFilesByName(JSON_FILENAME); let content = JSON.stringify(data); if (files.hasNext()) { files.next().setContent(content); } else { folder.createFile(JSON_FILENAME, content, MimeType.PLAIN_TEXT); } } /** * פונקציה לחיתוך קובץ WAV בגוגל סקריפט ברמת ה-Bytes (ללא תוספים) */ function smartTrimWavGas(sourceBlob, secondsToSkip) { let gasBytes = sourceBlob.getBytes(); // המרת הבתים ממערך חתום (של GAS) למערך לא-חתום (של JS) לצורך קריאת משתנים מדויקת let buffer = new ArrayBuffer(gasBytes.length); let uint8View = new Uint8Array(buffer); for (let i = 0; i < gasBytes.length; i++) { uint8View[i] = gasBytes[i] < 0 ? gasBytes[i] + 256 : gasBytes[i]; } let dataView = new DataView(buffer); // שליפת ByteRate ו-BlockAlign מה-Header let byteRate = dataView.getUint32(28, true); // little-endian let blockAlign = dataView.getUint16(32, true); // little-endian let bytesToSkip = Math.floor((secondsToSkip * byteRate) / blockAlign) * blockAlign; let dataStart = 44 + bytesToSkip; if (dataStart >= gasBytes.length) { return null; // זמן החיתוך ארוך מהקובץ } let newDataLength = gasBytes.length - dataStart; // בניית קובץ חדש בזיכרון let newBuffer = new ArrayBuffer(44 + newDataLength); let newUint8View = new Uint8Array(newBuffer); let newDataView = new DataView(newBuffer); // העתקת ה-Header המקורי for (let i = 0; i < 44; i++) { newUint8View[i] = uint8View[i]; } // העתקת נתוני הקול שנשארו (החל מהנקודה שחתכנו) for (let i = 0; i < newDataLength; i++) { newUint8View[44 + i] = uint8View[dataStart + i]; } // עדכון גדלים ב-Header let newSubChunk2Size = newDataLength; let newChunkSize = newSubChunk2Size + 36; newDataView.setUint32(4, newChunkSize, true); newDataView.setUint32(40, newSubChunk2Size, true); // המרה חזרה למערך בתים חתום ש-GAS יודע לעבוד איתו let finalGasBytes = []; for (let i = 0; i < newUint8View.length; i++) { let b = newUint8View[i]; finalGasBytes.push(b > 127 ? b - 256 : b); } return Utilities.newBlob(finalGasBytes, 'audio/wav', 'trimmed_' + Date.now() + '.wav'); } -
@BEN-ZION תודה, בכל זאת אעדיף ליצור זאת בעצמי תוך בדיקה שהכל עובד כשורה, חשבתי מראש על אחסון בדרייב אבל אני פחות מעדיף, אברר את זה עם ג׳מיני..
אגב, את הקודים האלו כתבת ב-AI, נכון?
א"כ באיזה AI? -
@CUBASE גימני פרו
-
@BEN-ZION
מה זה פרו?
3.1 או בסטודיו? -
@אA בגימני הרגיל במצב פרו
-
@BEN-ZION אני כנ"ל משתמש ב-AI Studio כיון ששם ה-3.1 Pro הוא חינמי ללא הגבלה
-
@CUBASE
דווקא יש לו מגבלה (או שרק אני הצלחתי להגיע אליה...לא נראה לי
)
אבל אפשר לעבוד איתו הרבה זמן -
@אA כנראה מעומס הקשר, זו מגבלה טכנית.
-
@CUBASE
כלומר?
שהשיחה ארוכה מידי? -
@אA כן, קרה לי כמה פעמים, מוחקים הודעות לא חשובות, מבקשים ממנו סיכום ומתחילים שיחה חדשה.
-
@CUBASE
כי הוא כותב הגעת להגבלה -
@אA מה כתוב בדיוק?