videoscriber-backend / api /hf /post-transcribe.js
Rimas Kavaliauskas
Add Telegram proxy support for HF outbound calls
7ad6b9b
import { verifyHfIncomingRequest } from '../../lib/hf-bridge.js';
import { getConfig } from '../../lib/config.js';
import { upsertTranscriptPayload } from '../../services/archive/upsert-transcript-payload.js';
import { normalizeVideoByKey } from '../../services/archive/normalize-video.js';
function setCors(res) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST,OPTIONS');
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, X-HF-Shared-Secret, X-HF-Timestamp, X-HF-Signature, X-Telegram-Bot-Api-Secret-Token'
);
}
function normalizeBaseUrl(baseUrl) {
return String(baseUrl || '').trim().replace(/\/+$/, '');
}
async function forwardTelegramUpdateToHf({ cfg, update }) {
const secret = String(cfg.telegramWebhookSecret || '').trim();
const hfBase = normalizeBaseUrl(cfg.hfBackendBaseUrl);
if (!hfBase) {
const error = new Error('HF_BACKEND_BASE_URL is not configured');
error.status = 503;
throw error;
}
const targetUrl = `${hfBase}/api/telegram/webhook/${encodeURIComponent(secret)}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Telegram-Bot-Api-Secret-Token': secret,
},
body: JSON.stringify(update || {}),
signal: controller.signal,
});
if (!response.ok) {
const bodyText = await response.text().catch(() => '');
const error = new Error(
`HF telegram worker rejected webhook update (${response.status})${bodyText ? `: ${bodyText.slice(0, 220)}` : ''}`
);
error.status = 502;
throw error;
}
} catch (error) {
if (error?.name === 'AbortError') {
const timeoutError = new Error('HF telegram worker timeout');
timeoutError.status = 502;
throw timeoutError;
}
throw error;
} finally {
clearTimeout(timeout);
}
}
export default async function handler(req, res) {
setCors(res);
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const cfg = getConfig();
const telegramHeaderSecret = String(req.headers['x-telegram-bot-api-secret-token'] || '').trim();
if (telegramHeaderSecret) {
const expectedSecret = String(cfg.telegramWebhookSecret || '').trim();
if (!expectedSecret) {
return res.status(503).json({
message: 'TELEGRAM_WEBHOOK_SECRET is not configured',
});
}
if (telegramHeaderSecret !== expectedSecret) {
return res.status(401).json({
message: 'Unauthorized Telegram webhook request',
});
}
try {
await forwardTelegramUpdateToHf({
cfg,
update: req.body && typeof req.body === 'object' ? req.body : {},
});
return res.status(200).json({ ok: true, accepted: true });
} catch (error) {
return res.status(Number(error?.status) || 502).json({
message: error?.message || 'HF telegram worker unavailable',
});
}
}
const auth = verifyHfIncomingRequest({
headers: req.headers || {},
body: req.body || {},
});
if (!auth.ok) {
return res.status(401).json({
message: 'Unauthorized HF bridge request',
reason: auth.reason,
});
}
try {
const body = req.body || {};
if (String(body.action || '').trim() === 'telegram_proxy') {
const method = String(body.method || '').trim();
const payload = body.payload && typeof body.payload === 'object' ? body.payload : null;
if (!method || !payload) {
return res.status(400).json({
message: 'telegram_proxy requires { method, payload }',
});
}
const botToken = String(cfg.telegramBotToken || '').trim();
if (!botToken) {
return res.status(503).json({
message: 'TELEGRAM_BOT_TOKEN is not configured on bridge server',
});
}
const tgResponse = await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const tgJson = await tgResponse.json().catch(() => ({}));
if (!tgResponse.ok || tgJson?.ok === false) {
return res.status(502).json({
message: 'Telegram proxy call failed',
telegram_error: tgJson?.description || `status ${tgResponse.status}`,
});
}
return res.status(200).json({
ok: true,
result: tgJson?.result || null,
});
}
if (String(body.action || '').trim() === 'telegram_proxy_document') {
const payload = body.payload && typeof body.payload === 'object' ? body.payload : null;
if (!payload) {
return res.status(400).json({
message: 'telegram_proxy_document requires { payload }',
});
}
const chatId = String(payload.chat_id || '').trim();
const filename = String(payload.filename || 'artifact.md').trim();
const caption = String(payload.caption || '').trim();
const content = String(payload.content || '');
if (!chatId) {
return res.status(400).json({
message: 'telegram_proxy_document payload.chat_id is required',
});
}
const botToken = String(cfg.telegramBotToken || '').trim();
if (!botToken) {
return res.status(503).json({
message: 'TELEGRAM_BOT_TOKEN is not configured on bridge server',
});
}
const formData = new FormData();
formData.append('chat_id', chatId);
if (caption) formData.append('caption', caption);
formData.append(
'document',
new Blob([content], { type: 'text/markdown;charset=utf-8' }),
filename
);
const tgResponse = await fetch(`https://api.telegram.org/bot${botToken}/sendDocument`, {
method: 'POST',
body: formData,
});
const tgJson = await tgResponse.json().catch(() => ({}));
if (!tgResponse.ok || tgJson?.ok === false) {
return res.status(502).json({
message: 'Telegram proxy document call failed',
telegram_error: tgJson?.description || `status ${tgResponse.status}`,
});
}
return res.status(200).json({
ok: true,
result: tgJson?.result || null,
});
}
const sourceUrl = String(body.source_url || '').trim();
const transcript = String(body.transcript || '').trim();
const sourceHash = String(body.source_hash || '').trim() || null;
const transcriptSource = String(body.transcript_source || body.source || '').trim() || 'vercel-transcribe';
const userId = String(body.user_id || '').trim() || null;
const targetLanguage = String(body.target_language || '').trim() || null;
const runPostprocess = body.run_postprocess !== false;
const waitForCompletion = body.wait_for_completion === true;
const upsert = await upsertTranscriptPayload({
sourceUrl,
transcript,
sourceHash,
transcriptSource,
userId,
});
if (!runPostprocess) {
return res.status(200).json({
accepted: true,
video_key: upsert.video_key,
postprocess: 'skipped',
});
}
if (waitForCompletion) {
const normalized = await normalizeVideoByKey(upsert.video_key, { targetLanguage });
return res.status(200).json({
accepted: true,
video_key: upsert.video_key,
postprocess: 'completed',
normalized,
});
}
// HF-side runtime branch:
// this endpoint can return quickly while normalization continues in background.
// This pattern is suitable for long-running HF backend processes.
normalizeVideoByKey(upsert.video_key, { targetLanguage }).catch((error) => {
console.error(`[hf-post-transcribe] normalize failed for ${upsert.video_key}:`, error);
});
return res.status(202).json({
accepted: true,
video_key: upsert.video_key,
postprocess: 'queued',
});
} catch (error) {
return res.status(Number(error?.status) || 500).json({
message: error?.message || 'HF post-transcribe processing failed',
});
}
}