mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
init
This commit is contained in:
7
thirdeye/meet_extension/background.js
Normal file
7
thirdeye/meet_extension/background.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* ThirdEye Meet — Service Worker (Manifest V3 background)
|
||||
* Minimal: just keeps the extension alive and relays messages.
|
||||
*/
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
console.log("[ThirdEye] Meet extension installed.");
|
||||
});
|
||||
255
thirdeye/meet_extension/content.js
Normal file
255
thirdeye/meet_extension/content.js
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* ThirdEye Meet Recorder — Content Script
|
||||
* Injected into meet.google.com pages.
|
||||
* Uses Web Speech API (Chrome built-in) for live transcription.
|
||||
* Buffers transcript and POSTs chunks to ThirdEye backend every CHUNK_INTERVAL_MS.
|
||||
*/
|
||||
|
||||
const CHUNK_INTERVAL_MS = 30000; // Send a chunk every 30 seconds
|
||||
const MAX_BUFFER_CHARS = 8000; // ~2000 tokens — safe for LLM processing
|
||||
|
||||
let recognition = null;
|
||||
let isRecording = false;
|
||||
let transcriptBuffer = "";
|
||||
let meetingId = null;
|
||||
let backendUrl = "http://localhost:8000";
|
||||
let ingestSecret = "thirdeye_meet_secret_change_me";
|
||||
let groupId = "meet_sessions";
|
||||
let chunkTimer = null;
|
||||
let chunkCount = 0;
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function getMeetingId() {
|
||||
// Extract meeting code from URL: meet.google.com/abc-defg-hij
|
||||
const match = window.location.pathname.match(/\/([a-z]{3}-[a-z]{4}-[a-z]{3})/i);
|
||||
if (match) return match[1];
|
||||
// Fallback: use timestamp-based ID
|
||||
return `meet_${Date.now()}`;
|
||||
}
|
||||
|
||||
function getParticipantName() {
|
||||
// Try to find the user's own name from the Meet DOM
|
||||
const nameEl = document.querySelector('[data-self-name]');
|
||||
if (nameEl) return nameEl.getAttribute('data-self-name');
|
||||
const profileEl = document.querySelector('img[data-iml]');
|
||||
if (profileEl && profileEl.alt) return profileEl.alt;
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// --- Transport ---
|
||||
|
||||
async function sendChunkToBackend(text, isFinal = false) {
|
||||
if (!text || text.trim().length < 10) return; // Don't send near-empty chunks
|
||||
|
||||
const payload = {
|
||||
meeting_id: meetingId,
|
||||
group_id: groupId,
|
||||
chunk_index: chunkCount++,
|
||||
text: text.trim(),
|
||||
speaker: getParticipantName(),
|
||||
timestamp: new Date().toISOString(),
|
||||
is_final: isFinal,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${backendUrl}/api/meet/ingest`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-ThirdEye-Secret": ingestSecret,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.warn(`[ThirdEye] Backend rejected chunk: ${res.status}`);
|
||||
} else {
|
||||
console.log(`[ThirdEye] Chunk ${payload.chunk_index} sent (${text.length} chars)`);
|
||||
chrome.runtime.sendMessage({
|
||||
type: "CHUNK_SENT",
|
||||
chunkIndex: payload.chunk_index,
|
||||
charCount: text.length,
|
||||
meetingId: meetingId,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[ThirdEye] Failed to send chunk: ${err.message}`);
|
||||
// Buffer is NOT cleared on failure — next flush will include this text
|
||||
transcriptBuffer = text + "\n" + transcriptBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Periodic flush ---
|
||||
|
||||
function flushBuffer() {
|
||||
if (transcriptBuffer.trim().length > 0) {
|
||||
sendChunkToBackend(transcriptBuffer);
|
||||
transcriptBuffer = "";
|
||||
}
|
||||
}
|
||||
|
||||
function startChunkTimer() {
|
||||
if (chunkTimer) clearInterval(chunkTimer);
|
||||
chunkTimer = setInterval(flushBuffer, CHUNK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopChunkTimer() {
|
||||
if (chunkTimer) {
|
||||
clearInterval(chunkTimer);
|
||||
chunkTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Web Speech API ---
|
||||
|
||||
function initSpeechRecognition() {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
if (!SpeechRecognition) {
|
||||
console.error("[ThirdEye] Web Speech API not available in this browser.");
|
||||
chrome.runtime.sendMessage({ type: "ERROR", message: "Web Speech API not supported." });
|
||||
return null;
|
||||
}
|
||||
|
||||
const rec = new SpeechRecognition();
|
||||
rec.continuous = true; // Don't stop after first pause
|
||||
rec.interimResults = true; // Get partial results while speaking
|
||||
rec.lang = "en-US"; // Change if needed
|
||||
rec.maxAlternatives = 1;
|
||||
|
||||
rec.onstart = () => {
|
||||
console.log("[ThirdEye] Speech recognition started.");
|
||||
chrome.runtime.sendMessage({ type: "STATUS", status: "recording", meetingId });
|
||||
};
|
||||
|
||||
rec.onresult = (event) => {
|
||||
let newText = "";
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
const result = event.results[i];
|
||||
if (result.isFinal) {
|
||||
newText += result[0].transcript + " ";
|
||||
}
|
||||
// We only accumulate FINAL results to avoid duplicates from interim results
|
||||
}
|
||||
|
||||
if (newText.trim()) {
|
||||
transcriptBuffer += newText;
|
||||
// Guard: if buffer is getting huge, flush early
|
||||
if (transcriptBuffer.length > MAX_BUFFER_CHARS) {
|
||||
flushBuffer();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
rec.onerror = (event) => {
|
||||
console.warn(`[ThirdEye] Speech recognition error: ${event.error}`);
|
||||
if (event.error === "not-allowed") {
|
||||
chrome.runtime.sendMessage({
|
||||
type: "ERROR",
|
||||
message: "Microphone permission denied. Allow mic access in Chrome settings.",
|
||||
});
|
||||
} else if (event.error !== "no-speech") {
|
||||
// Restart on non-fatal errors
|
||||
setTimeout(() => { if (isRecording) rec.start(); }, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
rec.onend = () => {
|
||||
console.log("[ThirdEye] Speech recognition ended.");
|
||||
if (isRecording) {
|
||||
// Auto-restart: Chrome's Web Speech API stops after ~60s of silence
|
||||
setTimeout(() => { if (isRecording) rec.start(); }, 250);
|
||||
}
|
||||
};
|
||||
|
||||
return rec;
|
||||
}
|
||||
|
||||
// --- Public controls (called from popup via chrome.tabs.sendMessage) ---
|
||||
|
||||
async function startRecording(config = {}) {
|
||||
if (isRecording) return;
|
||||
|
||||
// Load config from storage or use provided values
|
||||
const stored = await chrome.storage.sync.get(["backendUrl", "ingestSecret", "groupId"]);
|
||||
backendUrl = config.backendUrl || stored.backendUrl || "http://localhost:8000";
|
||||
ingestSecret = config.ingestSecret || stored.ingestSecret || "thirdeye_meet_secret_change_me";
|
||||
groupId = config.groupId || stored.groupId || "meet_sessions";
|
||||
|
||||
meetingId = getMeetingId();
|
||||
chunkCount = 0;
|
||||
transcriptBuffer = "";
|
||||
isRecording = true;
|
||||
|
||||
recognition = initSpeechRecognition();
|
||||
if (!recognition) {
|
||||
isRecording = false;
|
||||
return;
|
||||
}
|
||||
|
||||
recognition.start();
|
||||
startChunkTimer();
|
||||
|
||||
// Notify backend that a new meeting has started
|
||||
try {
|
||||
await fetch(`${backendUrl}/api/meet/start`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-ThirdEye-Secret": ingestSecret,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
meeting_id: meetingId,
|
||||
group_id: groupId,
|
||||
started_at: new Date().toISOString(),
|
||||
speaker: getParticipantName(),
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("[ThirdEye] Could not notify backend of meeting start:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopRecording() {
|
||||
if (!isRecording) return;
|
||||
|
||||
isRecording = false;
|
||||
stopChunkTimer();
|
||||
|
||||
if (recognition) {
|
||||
recognition.stop();
|
||||
recognition = null;
|
||||
}
|
||||
|
||||
// Send the final buffered chunk marked as final
|
||||
await sendChunkToBackend(transcriptBuffer, true);
|
||||
transcriptBuffer = "";
|
||||
|
||||
chrome.runtime.sendMessage({ type: "STATUS", status: "stopped", meetingId });
|
||||
console.log("[ThirdEye] Recording stopped.");
|
||||
}
|
||||
|
||||
// --- Message listener (from popup) ---
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.type === "START_RECORDING") {
|
||||
startRecording(msg.config || {}).then(() => sendResponse({ ok: true }));
|
||||
return true; // async
|
||||
}
|
||||
if (msg.type === "STOP_RECORDING") {
|
||||
stopRecording().then(() => sendResponse({ ok: true }));
|
||||
return true;
|
||||
}
|
||||
if (msg.type === "GET_STATUS") {
|
||||
sendResponse({
|
||||
isRecording,
|
||||
meetingId,
|
||||
bufferLength: transcriptBuffer.length,
|
||||
chunkCount,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[ThirdEye] Content script loaded on", window.location.href);
|
||||
BIN
thirdeye/meet_extension/icon128.png
Normal file
BIN
thirdeye/meet_extension/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
BIN
thirdeye/meet_extension/icon16.png
Normal file
BIN
thirdeye/meet_extension/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
BIN
thirdeye/meet_extension/icon48.png
Normal file
BIN
thirdeye/meet_extension/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
33
thirdeye/meet_extension/manifest.json
Normal file
33
thirdeye/meet_extension/manifest.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "ThirdEye Meet Recorder",
|
||||
"version": "1.0.0",
|
||||
"description": "Captures Google Meet transcripts and sends them to your ThirdEye knowledge base.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"scripting"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://meet.google.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://meet.google.com/*"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "ThirdEye Meet Recorder"
|
||||
},
|
||||
"icons": {
|
||||
"16": "icon16.png",
|
||||
"48": "icon48.png",
|
||||
"128": "icon128.png"
|
||||
}
|
||||
}
|
||||
144
thirdeye/meet_extension/popup.html
Normal file
144
thirdeye/meet_extension/popup.html
Normal file
@@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ThirdEye Meet</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
width: 320px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 13px;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.header {
|
||||
background: #1e293b;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
.logo { font-weight: 700; font-size: 15px; color: #38bdf8; }
|
||||
.badge {
|
||||
font-size: 10px;
|
||||
background: #0ea5e9;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.section { padding: 12px 16px; border-bottom: 1px solid #1e293b; }
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.status-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.status-dot.idle { background: #475569; }
|
||||
.status-dot.recording { background: #ef4444; animation: pulse 1.2s infinite; }
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; } 50% { opacity: 0.4; }
|
||||
}
|
||||
.meeting-id {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-family: monospace;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.stat { font-size: 11px; color: #64748b; }
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.btn:hover { opacity: 0.85; }
|
||||
.btn-start { background: #0ea5e9; color: white; }
|
||||
.btn-stop { background: #ef4444; color: white; }
|
||||
.btn-disabled { background: #334155; color: #64748b; cursor: not-allowed; }
|
||||
label { display: block; font-size: 11px; color: #94a3b8; margin-bottom: 3px; }
|
||||
input {
|
||||
width: 100%;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
color: #e2e8f0;
|
||||
border-radius: 5px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
input:focus { outline: none; border-color: #0ea5e9; }
|
||||
.save-btn {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
color: #94a3b8;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
.save-btn:hover { border-color: #0ea5e9; color: #0ea5e9; }
|
||||
.not-meet {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
.not-meet strong { display: block; color: #94a3b8; margin-bottom: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<span class="logo">👁 ThirdEye</span>
|
||||
<span class="badge">Meet</span>
|
||||
</div>
|
||||
|
||||
<div id="not-meet" class="not-meet" style="display:none;">
|
||||
<strong>Not on Google Meet</strong>
|
||||
Open a meeting at meet.google.com to start recording.
|
||||
</div>
|
||||
|
||||
<div id="meet-ui" style="display:none;">
|
||||
<div class="section">
|
||||
<div class="status-row">
|
||||
<div>
|
||||
<span class="status-dot idle" id="status-dot"></span>
|
||||
<span id="status-label">Idle</span>
|
||||
</div>
|
||||
<span class="stat" id="chunks-label">0 chunks sent</span>
|
||||
</div>
|
||||
<div class="meeting-id" id="meeting-id-label">Meeting ID: —</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<button class="btn btn-start" id="main-btn">▶ Start Recording</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<label>Backend URL</label>
|
||||
<input type="text" id="backend-url" placeholder="http://localhost:8000" />
|
||||
<label>Group ID</label>
|
||||
<input type="text" id="group-id" placeholder="meet_sessions" />
|
||||
<label>Ingest Secret</label>
|
||||
<input type="password" id="ingest-secret" placeholder="thirdeye_meet_secret_change_me" />
|
||||
<button class="save-btn" id="save-btn">Save Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
109
thirdeye/meet_extension/popup.js
Normal file
109
thirdeye/meet_extension/popup.js
Normal file
@@ -0,0 +1,109 @@
|
||||
let currentTab = null;
|
||||
let isRecording = false;
|
||||
|
||||
async function getActiveTab() {
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
return tabs[0] || null;
|
||||
}
|
||||
|
||||
function isMeetTab(tab) {
|
||||
return tab && tab.url && tab.url.includes("meet.google.com");
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get(["backendUrl", "ingestSecret", "groupId"]);
|
||||
document.getElementById("backend-url").value = stored.backendUrl || "http://localhost:8000";
|
||||
document.getElementById("ingest-secret").value = stored.ingestSecret || "";
|
||||
document.getElementById("group-id").value = stored.groupId || "meet_sessions";
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const backendUrl = document.getElementById("backend-url").value.trim();
|
||||
const ingestSecret = document.getElementById("ingest-secret").value.trim();
|
||||
const groupId = document.getElementById("group-id").value.trim();
|
||||
await chrome.storage.sync.set({ backendUrl, ingestSecret, groupId });
|
||||
const btn = document.getElementById("save-btn");
|
||||
btn.textContent = "✓ Saved";
|
||||
setTimeout(() => { btn.textContent = "Save Settings"; }, 1500);
|
||||
}
|
||||
|
||||
function setRecordingUI(recording, meetingId, chunks) {
|
||||
isRecording = recording;
|
||||
const dot = document.getElementById("status-dot");
|
||||
const label = document.getElementById("status-label");
|
||||
const btn = document.getElementById("main-btn");
|
||||
const meetLabel = document.getElementById("meeting-id-label");
|
||||
const chunksLabel = document.getElementById("chunks-label");
|
||||
|
||||
dot.className = "status-dot " + (recording ? "recording" : "idle");
|
||||
label.textContent = recording ? "Recording…" : "Idle";
|
||||
btn.className = "btn " + (recording ? "btn-stop" : "btn-start");
|
||||
btn.textContent = recording ? "■ Stop Recording" : "▶ Start Recording";
|
||||
meetLabel.textContent = meetingId ? `Meeting ID: ${meetingId}` : "Meeting ID: —";
|
||||
chunksLabel.textContent = `${chunks || 0} chunks sent`;
|
||||
}
|
||||
|
||||
async function getTabStatus() {
|
||||
if (!currentTab) return;
|
||||
try {
|
||||
const status = await chrome.tabs.sendMessage(currentTab.id, { type: "GET_STATUS" });
|
||||
setRecordingUI(status.isRecording, status.meetingId, status.chunkCount);
|
||||
} catch {
|
||||
setRecordingUI(false, null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMainBtn() {
|
||||
if (!currentTab) return;
|
||||
const stored = await chrome.storage.sync.get(["backendUrl", "ingestSecret", "groupId"]);
|
||||
|
||||
if (!isRecording) {
|
||||
await chrome.tabs.sendMessage(currentTab.id, {
|
||||
type: "START_RECORDING",
|
||||
config: {
|
||||
backendUrl: stored.backendUrl || "http://localhost:8000",
|
||||
ingestSecret: stored.ingestSecret || "thirdeye_meet_secret_change_me",
|
||||
groupId: stored.groupId || "meet_sessions",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await chrome.tabs.sendMessage(currentTab.id, { type: "STOP_RECORDING" });
|
||||
}
|
||||
|
||||
// Poll for updated status
|
||||
setTimeout(getTabStatus, 500);
|
||||
}
|
||||
|
||||
// Listen for status updates from content script
|
||||
chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (msg.type === "STATUS") {
|
||||
setRecordingUI(msg.status === "recording", msg.meetingId, null);
|
||||
}
|
||||
if (msg.type === "CHUNK_SENT") {
|
||||
document.getElementById("chunks-label").textContent = `${msg.chunkIndex + 1} chunks sent`;
|
||||
document.getElementById("meeting-id-label").textContent = `Meeting ID: ${msg.meetingId}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Init
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
currentTab = await getActiveTab();
|
||||
|
||||
if (!isMeetTab(currentTab)) {
|
||||
document.getElementById("not-meet").style.display = "block";
|
||||
document.getElementById("meet-ui").style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("meet-ui").style.display = "block";
|
||||
document.getElementById("not-meet").style.display = "none";
|
||||
|
||||
await loadSettings();
|
||||
await getTabStatus();
|
||||
|
||||
document.getElementById("main-btn").addEventListener("click", handleMainBtn);
|
||||
document.getElementById("save-btn").addEventListener("click", saveSettings);
|
||||
|
||||
// Poll status every 5s while popup is open
|
||||
setInterval(getTabStatus, 5000);
|
||||
});
|
||||
11
thirdeye/meet_extension/test.py
Normal file
11
thirdeye/meet_extension/test.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import base64, os
|
||||
|
||||
# Minimal 1x1 white PNG
|
||||
PNG_1x1 = base64.b64decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI6QAAAABJRU5ErkJggg=='
|
||||
)
|
||||
|
||||
for size in [16, 48, 128]:
|
||||
with open(f'meet_extension/icon{size}.png', 'wb') as f:
|
||||
f.write(PNG_1x1)
|
||||
print(f' Created icon{size}.png (placeholder)')
|
||||
Reference in New Issue
Block a user