Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
extension.zip
# OS
.DS_Store

# Build outputs
/dist/
*.zip

# Local scripts cache or temp
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor
.idea/
.vscode/
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# TranscripTonic
Simple Google Meet transcripts. Private and open source.
Simple Google Meet transcripts. Private and open source. Works on Chrome and Firefox.

![marquee-large](/assets/marquee-large.png)

Expand All @@ -21,10 +21,17 @@ View video on [YouTube](https://www.youtube.com/watch?v=ARL6HbkakX4)


# Installation

## Chrome
<a href="https://chromewebstore.google.com/detail/ciepnfnceimjehngolkijpnbappkkiag" target="_blank">
<img src="https://developer.chrome.com/static/docs/webstore/branding/image/iNEddTyWiMfLSwFD6qGq.png" />
</a>

## Firefox
Firefox users can install the extension using the unpacked installation method described below, or wait for the official Firefox Add-ons store release.

**Note:** Firefox requires Manifest V2 format, so a separate `manifest-firefox.json` file is provided for Firefox compatibility.

<br />
<br />

Expand Down Expand Up @@ -69,7 +76,7 @@ When this happens, it might be possible to recover the transcript, but recovery
<br />

# Privacy policy
TranscripTonic Chrome extension does not collect any information from users in any manner, except anonymous errors and transcript download timestamp. All processing/transcript storage happens within the user's Chrome browser and does not leave the device, unless you configure a webhook and choose to post data to your webhook URL.
TranscripTonic browser extension does not collect any information from users in any manner, except anonymous errors and transcript download timestamp. All processing/transcript storage happens within the user's browser and does not leave the device, unless you configure a webhook and choose to post data to your webhook URL.

<br />
<br />
Expand All @@ -79,3 +86,26 @@ The transcript may not always be accurate and is only intended to aid in improvi

<br />
<br />

# Installing unpacked extension
This method works for both Chrome and Firefox browsers.

1. Download the unpacked extension zip file from GitHub using this [link](https://raw.githubusercontent.com/vivek-nexus/transcriptonic/refs/heads/main/extension-unpacked.zip)

## For Chrome:
2. Open `chrome://extensions` in a new Chrome tab
3. Enable "Developer mode" from top right corner
4. Drag and drop the unpacked extension zip file to complete the installation process
5. If drag and drop of zip file does not work, unzip the file. Click on "Load unpacked" in chrome extensions page and select the `extension-unpacked` folder to complete the installation process.

## For Firefox:
2. Open `about:debugging` in a new Firefox tab
3. Click "This Firefox" in the left sidebar
4. Click "Load Temporary Add-on"
5. Unzip the downloaded file and navigate to the `extension-unpacked` folder
6. **Important:** Select specifically the `manifest-firefox.json` file (NOT `manifest.json`)

**Note:** Remove unpacked extension when no longer needed. Your meeting data of unpacked extension and extension installed from official stores are stored separately.

<br />
<br />
Binary file removed extension-unpacked.zip
Binary file not shown.
150 changes: 112 additions & 38 deletions extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,55 +352,61 @@ function downloadTranscript(index, isWebhookEnabled) {
content += "Transcript saved using TranscripTonic Chrome extension (https://chromewebstore.google.com/detail/ciepnfnceimjehngolkijpnbappkkiag)"
content += "\n---------------"

const blob = new Blob([content], { type: "text/plain" })
if (isFirefox()) {
// Firefox: use message passing to meetings.html for download
sendDownloadToMeetingsPage(fileName, content, resolve, reject);
} else {
// Chrome: use downloads API
const blob = new Blob([content], { type: "text/plain" })

// Read the blob as a data URL
const reader = new FileReader()
// Read the blob as a data URL
const reader = new FileReader()

// Read the blob
reader.readAsDataURL(blob)
// Read the blob
reader.readAsDataURL(blob)

// Download as text file, once blob is read
reader.onload = function (event) {
if (event.target?.result) {
const dataUrl = event.target.result
// Download as text file, once blob is read
reader.onload = function (event) {
if (event.target?.result) {
const dataUrl = event.target.result

// Create a download with Chrome Download API
chrome.downloads.download({
// @ts-ignore
url: dataUrl,
filename: fileName,
conflictAction: "uniquify"
}).then(() => {
console.log("Transcript downloaded")
resolve("Transcript downloaded successfully")

// Increment anonymous transcript generated count to a Google sheet
fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, {
mode: "no-cors"
})
}).catch((err) => {
console.error(err)
// Create a download with Chrome Download API
chrome.downloads.download({
// @ts-ignore
url: dataUrl,
filename: "TranscripTonic/Transcript.txt",
filename: fileName,
conflictAction: "uniquify"
})
console.log("Invalid file name. Transcript downloaded to TranscripTonic directory with simple file name.")
resolve("Transcript downloaded successfully with default file name")
}).then(() => {
console.log("Transcript downloaded")
resolve("Transcript downloaded successfully")

// Increment anonymous transcript generated count to a Google sheet
fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, {
mode: "no-cors"
})
}).catch((err) => {
console.error(err)
chrome.downloads.download({
// @ts-ignore
url: dataUrl,
filename: "TranscripTonic/Transcript.txt",
conflictAction: "uniquify"
})
console.log("Invalid file name. Transcript downloaded to TranscripTonic directory with simple file name.")
resolve("Transcript downloaded successfully with default file name")

// Logs anonymous errors to a Google sheet for swift debugging
fetch(`https://script.google.com/macros/s/AKfycbwN-bVkVv3YX4qvrEVwG9oSup0eEd3R22kgKahsQ3bCTzlXfRuaiO7sUVzH9ONfhL4wbA/exec?version=${chrome.runtime.getManifest().version}&code=009&error=${encodeURIComponent(err)}&meetingSoftware=${meeting.meetingSoftware}`, { mode: "no-cors" })
// Logs anonymous errors to a Google sheet for swift debugging
fetch(`https://script.google.com/macros/s/AKfycbwN-bVkVv3YX4qvrEVwG9oSup0eEd3R22kgKahsQ3bCTzlXfRuaiO7sUVzH9ONfhL4wbA/exec?version=${chrome.runtime.getManifest().version}&code=009&error=${encodeURIComponent(err)}&meetingSoftware=${meeting.meetingSoftware}`, { mode: "no-cors" })

// Increment anonymous transcript generated count to a Google sheet
fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, {
mode: "no-cors"
// Increment anonymous transcript generated count to a Google sheet
fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, {
mode: "no-cors"
})
})
})
}
else {
reject({ errorCode: "009", errorMessage: "Failed to read blob" })
}
else {
reject({ errorCode: "009", errorMessage: "Failed to read blob" })
}
}
}
}
Expand Down Expand Up @@ -669,4 +675,72 @@ function registerContentScripts(showNotification = true) {
})
})
})
}

function isFirefox() {
// @ts-ignore - browser is a Firefox-specific global
return typeof browser !== 'undefined' && /firefox/i.test(navigator.userAgent);
}

/**
* Firefox-compatible download handler that sends blob to meetings.html
* @param {string} fileName
* @param {string} content
* @param {Function} resolve
* @param {Function} reject
*/
function sendDownloadToMeetingsPage(fileName, content, resolve, reject) {
// Find or open meetings.html, then send the download message
chrome.tabs.query({}, function (tabs) {
let meetingsTab = tabs.find(tab => tab.url && tab.url.includes('meetings.html'));
if (meetingsTab && meetingsTab.id) {
chrome.tabs.update(meetingsTab.id, { active: true }, function () {
if (meetingsTab.id) {
chrome.tabs.sendMessage(
meetingsTab.id,
{
type: "download_transcript_blob",
fileName: fileName,
blobContent: content
},
function (response) {
if (response && response.success) {
resolve("Transcript downloaded successfully (Firefox)");
} else {
reject(new Error("Failed to trigger download in Firefox (meetings.html)"));
}
}
);
}
});
} else {
// Open meetings.html in a new tab
chrome.tabs.create({ url: chrome.runtime.getURL('meetings.html'), active: true }, function (newTab) {
// Wait for the tab to load, then send the message
const listener = function (tabId, changeInfo) {
if (tabId === newTab.id && changeInfo.status === 'complete') {
chrome.tabs.onUpdated.removeListener(listener);
if (newTab.id) {
chrome.tabs.sendMessage(
newTab.id,
{
type: "download_transcript_blob",
fileName: fileName,
blobContent: content
},
function (response) {
if (response && response.success) {
resolve("Transcript downloaded successfully (Firefox)");
} else {
reject(new Error("Failed to trigger download in Firefox (new meetings.html)"));
}
}
);
}
}
};
chrome.tabs.onUpdated.addListener(listener);
});
}
});
}
40 changes: 40 additions & 0 deletions extension/manifest-firefox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"manifest_version": 2,
"name": "TranscripTonic",
"version": "3.2.5",
"description": "Simple Google Meet transcripts. Private and open source.",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"icons": {
"128": "icon.png"
},
"content_scripts": [
{
"js": [
"content.js"
],
"run_at": "document_end",
"matches": [
"https://meet.google.com/*"
],
"exclude_matches": [
"https://meet.google.com/"
]
}
],
"permissions": [
"storage",
"https://meet.google.com/*",
"https://*/*"
],
"background": {
"scripts": [
"background.js"
]
},
"web_accessible_resources": [
"meetings.html"
]
}
9 changes: 8 additions & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"_comment": "This is the Chrome/Manifest V3 version. Firefox users should use manifest-firefox.json (Manifest V2)",
"manifest_version": 3,
"name": "TranscripTonic",
"version": "3.2.5",
Expand Down Expand Up @@ -43,5 +44,11 @@
],
"background": {
"service_worker": "background.js"
}
},
"web_accessible_resources": [
{
"resources": ["icon.png", "popup.html", "meetings.html"],
"matches": ["https://meet.google.com/*"]
}
]
}
27 changes: 27 additions & 0 deletions extension/meetings.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,31 @@ function getDuration(meetingStartTimestamp, meetingEndTimestamp) {
return durationHours > 0
? `${durationHours}h ${remainingMinutes}m`
: `${durationMinutes}m`
}

// Add Firefox download support
if (typeof browser !== 'undefined' && /firefox/i.test(navigator.userAgent)) {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'download_transcript_blob') {
try {
const blob = new Blob([message.blobContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = message.fileName;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
if (sendResponse) sendResponse({ success: true });
} catch (e) {
if (sendResponse) sendResponse({ success: false });
}
return true;
}
});
// If meetings.html is opened by the background script, focus the window
window.focus();
}
Loading