Livrare Lot 3 (Frontend): aplicație web, aplicație mobilă Android, extensie browser
- Surse complete web (React/Vite) + mobil (React Native/Expo) + extensie (MV3) - Documentație de livrare: ghid utilizare, matrice trasabilitate cerințe, raport testare furnizor - Artefacte binare: imagine Docker didi-frontend:lot3-1.0, APK, extensie v3.2.6 + SHA256SUMS - Configurare adresă platformă externalizată (build args / .env / config.js) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
cec967f953
321 changed files with 80506 additions and 0 deletions
492
extension/background.js
Normal file
492
extension/background.js
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
// DIDI Analysis Tool - Background Service Worker (V3 API prin Kong, auth OIDC)
|
||||
// BUILD: 2026-07-15 — OIDC/PKCE (Keycloak) + Bearer prin Kong; context menus; URL/selecție
|
||||
|
||||
console.log('[DIDI BUILD] background.js loaded — build 2026-07-15 (OIDC)');
|
||||
|
||||
// config.js FIRST (defines self.DidiConfig used by auth.js), then the renderer
|
||||
// and the auth module. render.js is needed in the SW to build the standalone
|
||||
// result page (popup is closed by the time async polling finishes).
|
||||
importScripts('config.js', 'render.js', 'auth.js');
|
||||
|
||||
// Same JWT-protected routes the web app uses (nginx → Kong → agent-v3).
|
||||
const DIDI_API_BASE = self.DidiConfig.API_BASE;
|
||||
const DEFAULT_PLAN_TYPE = 1;
|
||||
|
||||
// === Result delivery + history (centralized) ===
|
||||
// Push the analysis to the originator tab (content modal) when applicable, and
|
||||
// open a new tab with a rendered standalone page for popup-origin requests
|
||||
// (popup is closed by the time async polling completes, so its callback dies).
|
||||
async function deliverAnalysis({ analysis, filename, originatorTabId, originatorContext }) {
|
||||
// 1) Save to local history (used by history.html). Ignore errors.
|
||||
try {
|
||||
const data = await chrome.storage.local.get(['didiHistory']);
|
||||
const history = data.didiHistory || [];
|
||||
history.unshift({
|
||||
analysis, filename,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: originatorContext === 'popup' ? 'popup' : 'tab',
|
||||
});
|
||||
if (history.length > 50) history.length = 50;
|
||||
await chrome.storage.local.set({ didiHistory: history });
|
||||
} catch (e) {
|
||||
console.warn('[BACKGROUND] history save failed:', e.message);
|
||||
}
|
||||
|
||||
// 2) Content-script origin (snip + video + context menu): push the modal back
|
||||
// into the page; if the tab is gone, fall back to a standalone result tab.
|
||||
if (originatorTabId != null && originatorContext === 'content') {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(originatorTabId, { action: 'showAnalysis', analysis, filename });
|
||||
} catch (e) {
|
||||
console.warn('[BACKGROUND] tabs.sendMessage failed (tab closed?), opening result tab:', e.message);
|
||||
originatorContext = 'popup';
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Popup origin (text/url): popup is closed, open a new tab with the rendered page.
|
||||
if (originatorContext === 'popup') {
|
||||
try {
|
||||
const html = self.DidiRender
|
||||
? self.DidiRender.renderStandalonePage(analysis)
|
||||
: `<pre>${JSON.stringify(analysis, null, 2)}</pre>`;
|
||||
// Service workers can't use URL.createObjectURL — build a data URL instead.
|
||||
const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(html);
|
||||
await chrome.tabs.create({ url: dataUrl });
|
||||
} catch (e) {
|
||||
console.error('[BACKGROUND] failed to open result tab:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== AUTH (OIDC/PKCE via auth.js) ==========
|
||||
|
||||
/** Authorization header for API calls; refreshes the access token when needed. */
|
||||
async function authHeaders() {
|
||||
const token = await self.DidiAuth.getValidToken();
|
||||
return { 'Authorization': `Bearer ${token}` };
|
||||
}
|
||||
|
||||
// ========== UPLOAD CONSENT (cerință caiet: consimțământ explicit fișiere/capturi) ==========
|
||||
|
||||
async function hasUploadConsent() {
|
||||
const data = await chrome.storage.local.get(['didiUploadConsent']);
|
||||
return data.didiUploadConsent === true;
|
||||
}
|
||||
|
||||
/** Ask the page (content script) to show the consent modal; resolves true if accepted. */
|
||||
async function ensureUploadConsent(tabId) {
|
||||
if (await hasUploadConsent()) return true;
|
||||
if (tabId == null) return false;
|
||||
try {
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { action: 'requestUploadConsent' });
|
||||
return resp?.accepted === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== MESSAGE LISTENER ==========
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
console.log('[BACKGROUND] Message received:', message.action);
|
||||
|
||||
if (message.action === 'login') {
|
||||
self.DidiAuth.login()
|
||||
.then((tokens) => sendResponse({ success: true, email: tokens.email, name: tokens.name }))
|
||||
.catch((error) => sendResponse({ success: false, error: error.message }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'getAuthStatus') {
|
||||
self.DidiAuth.getAuthState()
|
||||
.then((state) => sendResponse(state))
|
||||
.catch(() => sendResponse({ authenticated: false }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'logout') {
|
||||
self.DidiAuth.logout()
|
||||
.then(() => sendResponse({ success: true }))
|
||||
.catch(() => sendResponse({ success: true })); // local session is cleared regardless
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'snipCaptured') {
|
||||
console.log('[BACKGROUND] Snip captured:', message.rect);
|
||||
const tabId = sender.tab?.id ?? null;
|
||||
|
||||
// Ack the originator immediately so it doesn't sit on a stale callback for ~90s.
|
||||
// The actual result is delivered via chrome.tabs.sendMessage when polling completes.
|
||||
sendResponse({ success: true, accepted: true });
|
||||
|
||||
chrome.tabs.captureVisibleTab(null, { format: 'png' }, async (screenshotUrl) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('[BACKGROUND] Screenshot error:', chrome.runtime.lastError);
|
||||
if (tabId != null) chrome.tabs.sendMessage(tabId, { action: 'analysisFailed', error: chrome.runtime.lastError.message }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const croppedDataUrl = await cropScreenshot(screenshotUrl, message.rect, message.devicePixelRatio || 1);
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0];
|
||||
const filename = `didi-snip-${timestamp}.png`;
|
||||
|
||||
console.log('[BACKGROUND] Sending screenshot to DIDI API…');
|
||||
const analysis = await analyzeScreenshot(croppedDataUrl, filename);
|
||||
console.log('[BACKGROUND] Analysis complete:', analysis);
|
||||
await deliverAnalysis({ analysis, filename, originatorTabId: tabId, originatorContext: 'content' });
|
||||
} catch (error) {
|
||||
console.error('[BACKGROUND] Analysis failed:', error);
|
||||
if (tabId != null) chrome.tabs.sendMessage(tabId, { action: 'analysisFailed', error: error.message }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'analyzeText') {
|
||||
console.log('[BACKGROUND] Analyzing text, length:', message.text?.length);
|
||||
sendResponse({ success: true, accepted: true }); // popup will likely close — result delivered via new tab
|
||||
|
||||
analyzeTextContent(message.text)
|
||||
.then((analysis) => {
|
||||
const filename = `text-analysis-${new Date().toISOString().replace(/:/g, '-').split('.')[0]}.txt`;
|
||||
return deliverAnalysis({ analysis, filename, originatorTabId: null, originatorContext: 'popup' });
|
||||
})
|
||||
.catch((error) => console.error('[BACKGROUND] Text analysis failed:', error));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'analyzeVideo') {
|
||||
console.log('[BACKGROUND] Analyzing video, filename:', message.filename);
|
||||
const tabId = sender.tab?.id ?? null;
|
||||
sendResponse({ success: true, accepted: true });
|
||||
|
||||
analyzeVideo(message.dataUrl, message.filename)
|
||||
.then((analysis) => deliverAnalysis({ analysis, filename: message.filename, originatorTabId: tabId, originatorContext: 'content' }))
|
||||
.catch((error) => {
|
||||
console.error('[BACKGROUND] Video analysis failed:', error);
|
||||
if (tabId != null) chrome.tabs.sendMessage(tabId, { action: 'analysisFailed', error: error.message }).catch(() => {});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Social post extracted in-page (Facebook/Twitter extractors) → analyze it.
|
||||
if (message.action === 'contentExtracted') {
|
||||
const tabId = sender.tab?.id ?? null;
|
||||
const content = message.data || {};
|
||||
sendResponse({ success: true, accepted: true });
|
||||
|
||||
(async () => {
|
||||
const text = (content.text || '').trim();
|
||||
const externalLink = (content.links || []).find((l) => typeof l === 'string' && /^https?:/i.test(l));
|
||||
let analysis;
|
||||
if (text) {
|
||||
analysis = await analyzeTextContent(text);
|
||||
} else if (externalLink) {
|
||||
analysis = await analyzeUrl(externalLink);
|
||||
} else {
|
||||
throw new Error('Postarea selectată nu conține text sau link analizabil');
|
||||
}
|
||||
await deliverAnalysis({ analysis, filename: `${message.platform || 'social'}-post`, originatorTabId: tabId, originatorContext: 'content' });
|
||||
})().catch((error) => {
|
||||
console.error('[BACKGROUND] Social post analysis failed:', error);
|
||||
notifyTab(tabId, 'Analiza postării a eșuat: ' + error.message, 'error');
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'analyzeUrl') {
|
||||
console.log('[BACKGROUND] Analyzing URL:', message.url);
|
||||
sendResponse({ success: true, accepted: true });
|
||||
|
||||
analyzeUrl(message.url)
|
||||
.then((analysis) => {
|
||||
const filename = `url-analysis-${new Date().toISOString().replace(/:/g, '-').split('.')[0]}.txt`;
|
||||
return deliverAnalysis({ analysis, filename, originatorTabId: null, originatorContext: 'popup' });
|
||||
})
|
||||
.catch((error) => console.error('[BACKGROUND] URL analysis failed:', error));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// ========== INITIALIZATION + CONTEXT MENUS ==========
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
console.log('[DIDI] Extension V3 installed');
|
||||
chrome.contextMenus.removeAll(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: 'didi-analyze-image',
|
||||
title: 'Analizează imaginea cu DiDi',
|
||||
contexts: ['image'],
|
||||
});
|
||||
chrome.contextMenus.create({
|
||||
id: 'didi-analyze-selection',
|
||||
title: 'Analizează textul selectat cu DiDi',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
chrome.contextMenus.create({
|
||||
id: 'didi-analyze-page',
|
||||
title: 'Analizează pagina curentă cu DiDi',
|
||||
contexts: ['page'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** Toast helper for context-menu flows (best-effort; page may block injection). */
|
||||
function notifyTab(tabId, message, type = 'info') {
|
||||
if (tabId == null) return;
|
||||
chrome.tabs.sendMessage(tabId, { action: 'showToast', message, type }).catch(() => {});
|
||||
}
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
const tabId = tab?.id ?? null;
|
||||
try {
|
||||
// Make sure the content script is present (tabs opened before install/reload).
|
||||
if (tabId != null) {
|
||||
try {
|
||||
await chrome.scripting.executeScript({ target: { tabId }, files: ['render.js', 'content.js'] });
|
||||
} catch { /* injection blocked (chrome:// etc.) — fall back to result tab */ }
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'didi-analyze-image') {
|
||||
if (!info.srcUrl) return;
|
||||
if (!(await ensureUploadConsent(tabId))) {
|
||||
notifyTab(tabId, 'Analiza a fost anulată — este necesar consimțământul pentru trimiterea imaginii.', 'error');
|
||||
return;
|
||||
}
|
||||
notifyTab(tabId, 'DiDi analizează imaginea… rezultatul apare aici în 30–90s.');
|
||||
const analysis = await analyzeImageFromSrc(info.srcUrl);
|
||||
await deliverAnalysis({ analysis, filename: 'context-image', originatorTabId: tabId, originatorContext: 'content' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'didi-analyze-selection') {
|
||||
const text = (info.selectionText || '').trim();
|
||||
if (!text) return;
|
||||
notifyTab(tabId, 'DiDi analizează textul selectat… rezultatul apare aici în 30–90s.');
|
||||
const analysis = await analyzeTextContent(text);
|
||||
await deliverAnalysis({ analysis, filename: 'context-selection', originatorTabId: tabId, originatorContext: 'content' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'didi-analyze-page') {
|
||||
const pageUrl = info.pageUrl || tab?.url;
|
||||
if (!pageUrl || !/^https?:/i.test(pageUrl)) return;
|
||||
notifyTab(tabId, 'DiDi analizează pagina curentă… rezultatul apare aici în 30–90s.');
|
||||
const analysis = await analyzeUrl(pageUrl);
|
||||
await deliverAnalysis({ analysis, filename: 'context-url', originatorTabId: tabId, originatorContext: 'content' });
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BACKGROUND] Context menu analysis failed:', error);
|
||||
notifyTab(tabId, 'Analiza a eșuat: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// ========== SCREENSHOT CROP ==========
|
||||
|
||||
async function cropScreenshot(dataUrl, rect, devicePixelRatio = 1) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(dataUrl)
|
||||
.then(res => res.blob())
|
||||
.then(blob => createImageBitmap(blob))
|
||||
.then(imageBitmap => {
|
||||
const scaledLeft = rect.left * devicePixelRatio;
|
||||
const scaledTop = rect.top * devicePixelRatio;
|
||||
const scaledWidth = rect.width * devicePixelRatio;
|
||||
const scaledHeight = rect.height * devicePixelRatio;
|
||||
|
||||
const canvas = new OffscreenCanvas(rect.width, rect.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(imageBitmap, scaledLeft, scaledTop, scaledWidth, scaledHeight, 0, 0, rect.width, rect.height);
|
||||
return canvas.convertToBlob({ type: 'image/png' });
|
||||
})
|
||||
.then(blob => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
// Manual data URL → Blob decoder.
|
||||
// fetch(dataUrl) breaks in the MV3 service worker when MIME contains an
|
||||
// unquoted comma (e.g. MediaRecorder's `video/webm;codecs=vp9,opus`): the URL
|
||||
// parser splits on that first comma and treats the rest as the body, so the
|
||||
// "blob" ends up containing the literal text `opus;base64,...` instead of
|
||||
// decoded bytes. Splitting on the unambiguous `;base64,` marker avoids it.
|
||||
async function dataUrlToBlob(dataUrl) {
|
||||
const marker = ';base64,';
|
||||
const b64Idx = dataUrl.indexOf(marker);
|
||||
if (b64Idx >= 0) {
|
||||
const mime = dataUrl.substring(5, b64Idx); // strip leading "data:"
|
||||
const b64 = dataUrl.substring(b64Idx + marker.length);
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return new Blob([bytes], { type: mime });
|
||||
}
|
||||
const comma = dataUrl.indexOf(',');
|
||||
if (comma < 0) throw new Error('Invalid data URL');
|
||||
const mime = dataUrl.substring(5, comma);
|
||||
return new Blob([decodeURIComponent(dataUrl.substring(comma + 1))], { type: mime });
|
||||
}
|
||||
|
||||
// ========== V3 API: ANALYZE (async + polling, Bearer prin Kong) ==========
|
||||
// Same contract as the web app: POST /pipeline/analyze-async returns 202 +
|
||||
// session_id, poll /pipeline/:id/queue-status every 2s, then GET /:id/result.
|
||||
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes hard cap (video upper bound)
|
||||
|
||||
/** Submit an analyze-async request and poll until completion. */
|
||||
async function submitAndPoll(payload, kind) {
|
||||
const claims = await self.DidiAuth.getUserClaims();
|
||||
const body = { ...payload, ...claims, plan_type: DEFAULT_PLAN_TYPE };
|
||||
console.log(`[DIDI API] ${kind} async submit`, JSON.stringify(body).substring(0, 200));
|
||||
|
||||
const submitResp = await fetch(`${DIDI_API_BASE}/pipeline/analyze-async`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8', ...(await authHeaders()) },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!submitResp.ok) {
|
||||
const errText = await submitResp.text();
|
||||
throw new Error(`${kind} submit failed: ${submitResp.status} - ${errText}`);
|
||||
}
|
||||
|
||||
const submit = await submitResp.json();
|
||||
// Sync fallback: backend already returned a complete analysis (queue was unavailable)
|
||||
if (submit.async === false && submit.data) {
|
||||
console.log(`[DIDI API] ${kind} sync fallback (queue down) — returning result directly`);
|
||||
return submit.data;
|
||||
}
|
||||
|
||||
const sessionId = submit.data?.session_id;
|
||||
if (!sessionId) throw new Error(`${kind} submit response missing session_id: ${JSON.stringify(submit)}`);
|
||||
|
||||
console.log(`[DIDI API] ${kind} dispatched, session_id=${sessionId}, polling…`);
|
||||
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < POLL_TIMEOUT_MS) {
|
||||
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
||||
let data;
|
||||
try {
|
||||
const statusResp = await fetch(`${DIDI_API_BASE}/pipeline/${sessionId}/queue-status`, {
|
||||
method: 'GET',
|
||||
headers: await authHeaders(),
|
||||
});
|
||||
if (!statusResp.ok) {
|
||||
console.warn(`[DIDI API] poll ${sessionId}: HTTP ${statusResp.status}`);
|
||||
continue;
|
||||
}
|
||||
const statusJson = await statusResp.json();
|
||||
data = statusJson.data || {};
|
||||
} catch (e) {
|
||||
// Network blip — log and try again; do NOT swallow a server-side 'failed' status
|
||||
console.warn(`[DIDI API] poll network error (will retry): ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
console.log(`[DIDI API] poll ${sessionId}: status=${data.status} progress=${data._queue?.progress ?? '?'}%`);
|
||||
if (data.status === 'completed') {
|
||||
// queue-status may omit heavy result fields — fetch the full session
|
||||
try {
|
||||
const resultResp = await fetch(`${DIDI_API_BASE}/pipeline/${sessionId}/result`, {
|
||||
method: 'GET',
|
||||
headers: await authHeaders(),
|
||||
});
|
||||
if (resultResp.ok) {
|
||||
const resultJson = await resultResp.json();
|
||||
if (resultJson.data) return resultJson.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[DIDI API] result fetch failed, falling back to queue-status payload: ${e.message}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
if (data.status === 'failed') {
|
||||
throw new Error(`Analysis failed server-side: ${data.reason || 'no result returned'}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`Analysis timed out after ${POLL_TIMEOUT_MS / 1000}s (session ${sessionId})`);
|
||||
}
|
||||
|
||||
/** Upload a media blob via /media/upload (Bearer) → public_url. */
|
||||
async function uploadMediaBlob(blob, filename) {
|
||||
const claims = await self.DidiAuth.getUserClaims();
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, filename);
|
||||
formData.append('user_id', claims.user_id);
|
||||
|
||||
const resp = await fetch(`${DIDI_API_BASE}/media/upload`, {
|
||||
method: 'POST',
|
||||
headers: await authHeaders(),
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const errText = await resp.text();
|
||||
throw new Error(`Upload failed: ${resp.status} - ${errText}`);
|
||||
}
|
||||
|
||||
const json = await resp.json();
|
||||
const d = json.data || {};
|
||||
const url = d.public_url || d.download_url || d.url;
|
||||
if (!url) {
|
||||
throw new Error(`Upload succeeded but no URL in response: ${JSON.stringify(json).substring(0, 300)}`);
|
||||
}
|
||||
console.log('[DIDI API] Upload OK:', url);
|
||||
return url;
|
||||
}
|
||||
|
||||
async function analyzeTextContent(text) {
|
||||
return submitAndPoll({ text, media_type: 'text' }, 'text');
|
||||
}
|
||||
|
||||
async function analyzeUrl(url) {
|
||||
// Normalize www. prefix like the web app does
|
||||
const normalized = /^www\./i.test(url) ? `https://${url}` : url;
|
||||
return submitAndPoll({ url: normalized, media_type: 'url' }, 'url');
|
||||
}
|
||||
|
||||
async function analyzeScreenshot(dataUrl, filename) {
|
||||
console.log('[DIDI API] Screenshot upload + analyze…');
|
||||
const blob = await dataUrlToBlob(dataUrl);
|
||||
const imageUrl = await uploadMediaBlob(blob, filename);
|
||||
return submitAndPoll({ media_url: imageUrl, media_type: 'image' }, 'image');
|
||||
}
|
||||
|
||||
/** Analyze an image already hosted somewhere (context menu on <img>). */
|
||||
async function analyzeImageFromSrc(srcUrl) {
|
||||
console.log('[DIDI API] Image fetch + upload + analyze…', srcUrl.substring(0, 120));
|
||||
let blob;
|
||||
if (srcUrl.startsWith('data:')) {
|
||||
blob = await dataUrlToBlob(srcUrl);
|
||||
} else {
|
||||
const resp = await fetch(srcUrl);
|
||||
if (!resp.ok) throw new Error(`Nu am putut prelua imaginea (HTTP ${resp.status})`);
|
||||
blob = await resp.blob();
|
||||
}
|
||||
const ext = (blob.type.split('/')[1] || 'png').split(';')[0];
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0];
|
||||
const imageUrl = await uploadMediaBlob(blob, `didi-image-${timestamp}.${ext}`);
|
||||
return submitAndPoll({ media_url: imageUrl, media_type: 'image' }, 'image');
|
||||
}
|
||||
|
||||
async function analyzeVideo(dataUrl, filename) {
|
||||
console.log('[DIDI API] Video upload + analyze…');
|
||||
const blob = await dataUrlToBlob(dataUrl);
|
||||
console.log('[DIDI API] Video blob size:', blob.size, 'bytes, type:', blob.type);
|
||||
const videoUrl = await uploadMediaBlob(blob, filename);
|
||||
return submitAndPoll({ media_url: videoUrl, media_type: 'video' }, 'video');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue