הקלטת השיחה בלבד במודול תור
-
@CUBASE סביר להניח שזה באג
אתה צריך לכתוב קוד שיקרא מהלוג כמה שניות המתין למענה ואז לחתוך מתחילת הקובץ את מספר השניות פחות שניה אחת -
@אA כתב בהקלטת השיחה בלבד במודול תור:
@CUBASE
קודם כל תכתוב לאלחנן.קודם אני רוצה לדעת בוודאות שזה באג
@BEN-ZION כתב בהקלטת השיחה בלבד במודול תור:
אתה צריך לכתוב קוד שיקרא מהלוג כמה שניות המתין למענה ואז לחתוך מתחילת הקובץ את מספר השניות פחות שניה אחת
רעיון יפה, אם כי צריך לחשוב איך לבצע אותו,
לכאו' עם גוגל סקריפט שירות בכל פעם שמתקבל מייל של דו"ח שיחה,
אבל איך יתבצע החיתוך? -
@CUBASE זה קוד בPHP שחותך הוא לא משתמש בשרתים חיצוניים לחיתוך הוא מתבסס על זה שזה קובץ WAV וחותך ביטים מתוך הזיכרון בדקתי אצלי והוא עובד מעולה ניתן גם להמיר לגוגל סקריפט אני מאמין
הגדרתי לו להעלות לתיקייה חדשה
הוא שומר לעצמו את הקבצים שכבר טופלו כדי שלא יחתוך פעמיים את אותו קובץ
אתה יכול גם לאחסן אותו בשרת PHP ובגוגל לתזמן שבמייל נכנס יעיר אותו<?php // --- הגדרות מערכת --- $token = ""; // הטוקן שלך $logPath = "ivr2:Log/LogQueueOK.2026-03-31.ymgr"; // נתיב הלוג של התור $apiUrl = "https://www.call2all.co.il/ym/api/"; // --- הגדרות סביבה מקומית --- $tempDir = __DIR__ . '/temp_audio/'; if (!is_dir($tempDir)) { mkdir($tempDir, 0777, true); } $processedFile = __DIR__ . '/processed_calls.json'; // קובץ לשמירת הקלטות שכבר טופלו $logFile = __DIR__ . '/script_log.txt'; // קובץ לוג מקומי מפורט // --- פונקציית כתיבה ללוג --- function addLog($msg) { global $logFile; $timestamp = date('d/m/Y H:i:s'); $logMessage = "[$timestamp] $msg" . PHP_EOL; file_put_contents($logFile, $logMessage, FILE_APPEND); echo $logMessage . "<br>"; // הדפסה למסך במקביל } // טעינת רשימת השיחות שכבר טופלו בעבר $processedPaths = []; if (file_exists($processedFile)) { $processedPaths = json_decode(file_get_contents($processedFile), true) ?: []; } addLog("--- תחילת ריצת מערכת ---"); addLog("שולף את קובץ הלוג: $logPath"); // --- שלב 1: קריאת הלוג מימות המשיח --- $logData = getYmApi($apiUrl . "RenderYMGRFile", [ 'token' => $token, 'wath' => $logPath, 'convertType' => 'json', 'notLoadLang' => 1 ]); $data = json_decode($logData, true); if (!$data || !isset($data['data'])) { addLog("שגיאה: לא נמצאו נתונים בלוג או שהטוקן שגוי/הנתיב לא קיים."); die("שגיאה קריטית, קרא את קובץ הלוג לפרטים."); } addLog("קובץ הלוג נקרא בהצלחה, מתחיל סריקת רשומות..."); // --- שלב 2: עיבוד הרשומות --- foreach ($data['data'] as $row) { // בודק אם זו שיחה שנענתה ויש נתיב להקלטה if (isset($row['QueueStatus']) && $row['QueueStatus'] === 'ANSWER' && !empty($row['QueueRecordPath'])) { $remotePath = $row['QueueRecordPath']; $waitSeconds = (int)$row['QueueWaitingSeconds']; // בדיקה האם הקובץ כבר טופל בריצות קודמות if (in_array($remotePath, $processedPaths)) { continue; // מדלג בשקט לפריט הבא } // אם היה זמן המתנה, צריך לחתוך if ($waitSeconds > 0) { addLog("זוהתה שיחה חדשה. נתיב: $remotePath | זמן חיתוך נדרש: $waitSeconds שניות."); $localOrig = $tempDir . 'orig_' . md5($remotePath) . '.wav'; $localTrim = $tempDir . 'trim_' . md5($remotePath) . '.wav'; // הורדת הקובץ מימות המשיח addLog("מוריד את הקובץ..."); $fileContent = getYmApi($apiUrl . "DownloadFile", [ 'token' => $token, 'path' => $remotePath ]); if ($fileContent && strlen($fileContent) > 44) { file_put_contents($localOrig, $fileContent); addLog("הקובץ הורד ונשמר זמנית בהצלחה. גודל הקובץ: " . strlen($fileContent) . " בתים."); // ביצוע החיתוך הדינמי addLog("מתחיל תהליך חיתוך של $waitSeconds שניות מההתחלה..."); if (smartTrimWav($localOrig, $localTrim, $waitSeconds)) { addLog("הקובץ נחתך בהצלחה."); // --- יצירת נתיב חדש לתיקיית "חתוכים" --- // מחלצים את הנתיב עד לתיקייה ואת שם הקובץ ומכניסים את "חתוכים" באמצע $pathParts = pathinfo($remotePath); $newRemotePath = $pathParts['dirname'] . '/חתוכים/' . $pathParts['basename']; addLog("מעלה קובץ חדש לנתיב הבדיקות: $newRemotePath"); $uploadStatus = uploadToYm($apiUrl . "UploadFile", $token, $newRemotePath, $localTrim); $uploadRes = json_decode($uploadStatus, true); if (isset($uploadRes['responseStatus']) && $uploadRes['responseStatus'] === 'OK') { addLog("הקובץ הועלה בהצלחה."); // סימון הקובץ כ'טופל' כדי שלא ייבדק שוב $processedPaths[] = $remotePath; file_put_contents($processedFile, json_encode($processedPaths)); } else { addLog("שגיאה בהעלאת הקובץ: $uploadStatus"); } } else { addLog("שגיאה בחיתוך הקובץ (יתכן שזמן החיתוך ארוך מאורך הקובץ הכולל)."); } // ניקוי קבצים זמניים מהשרת המקומי @unlink($localOrig); @unlink($localTrim); } else { addLog("שגיאה: הקובץ ריק או קצר מדי (פחות מ-44 בתים). לא ניתן לחתוך."); } } else { // זמן המתנה הוא 0, אין צורך לחתוך, נסמן כטופל כדי לדלג עליו בהמשך addLog("שיחה בנתיב $remotePath ללא זמן המתנה (0 שניות). מסמן כטופל ומדלג."); $processedPaths[] = $remotePath; file_put_contents($processedFile, json_encode($processedPaths)); } addLog("--------------------------------------------------"); } } addLog("--- הריצה הסתיימה בהצלחה ---"); // ================= פונקציות עזר ================= /** * פונקציה לחיתוך קובץ WAV מבוסס Header ו-ByteRate ללא תוספים */ function smartTrimWav($sourcePath, $destPath, $secondsToSkip) { $fp = fopen($sourcePath, 'rb'); if (!$fp) return false; // קורא את כותרת ה-WAV (ה-44 בתים הראשונים) $header = fread($fp, 44); // שליפת ה-Byte Rate מתוך ה-Header של הקובץ (כדי לתמוך בכל קצב דגימה שהם יחזירו) $info = unpack('VbyteRate', substr($header, 28, 4)); $bytesPerSecond = $info['byteRate']; // הוספת Block Align שמיישר את הבתים לפי ערוצים (16bit = 2 bytes) $blockAlignInfo = unpack('vblockAlign', substr($header, 32, 2)); $blockAlign = $blockAlignInfo['blockAlign']; // חישוב כמות הבתים שיש לדלג, מחייב חלוקה מדויקת ב- BlockAlign כדי לא להשחית את גל הקול $bytesToSkip = floor(($secondsToSkip * $bytesPerSecond) / $blockAlign) * $blockAlign; $fileSize = filesize($sourcePath); $dataStart = 44 + $bytesToSkip; // מוודא שאנחנו לא מנסים לחתוך יותר אורך ממה שקיים בקובץ if ($dataStart >= $fileSize) { fclose($fp); return false; } fseek($fp, $dataStart); $data = stream_get_contents($fp); fclose($fp); // עדכון גדלים בכותרת הקובץ (תיקון ה-Header) $newSubChunk2Size = strlen($data); $newChunkSize = $newSubChunk2Size + 36; // הזרקת הגדלים החדשים (בפורמט 32-bit little-endian) $header = substr_replace($header, pack('V', $newChunkSize), 4, 4); $header = substr_replace($header, pack('V', $newSubChunk2Size), 40, 4); return file_put_contents($destPath, $header . $data); } /** * פונקציה לתקשורת API (מתודת POST) */ function getYmApi($url, $params) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // מונע שגיאות תעודת אבטחה בשרתים מסוימים $res = curl_exec($ch); curl_close($ch); return $res; } /** * פונקציה להעלאת קובץ בפורמט multipart/form-data */ function uploadToYm($url, $token, $path, $localFile) { $ch = curl_init(); $cFile = new CURLFile($localFile, 'audio/wav', 'file.wav'); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'token' => $token, 'path' => $path, 'file' => $cFile ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $res = curl_exec($ch); curl_close($ch); return $res; } ?> -
@BEN-ZION זה לא הבעיה, השאלה אם אפשר בגוגל סקריפט להוריד קובץ לזיכרון
-
@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 מה כתוב בדיוק?