mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
document to markdown
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -9,6 +9,7 @@ const s3 = require("../../services/s3.service");
|
||||
const mediaToken = require("../../services/mediaToken.service");
|
||||
const uploadProgress = require("../../services/uploadProgress.service");
|
||||
const { extractVideoMeta } = require("../../services/ffprobe.service");
|
||||
const documentConversion = require("../../services/documentConversion.service");
|
||||
const R = require('../../utils/response.util');
|
||||
const { paginate } = require("../../utils/paginate.util");
|
||||
const { adminExclude, jsonbSchemas, computedAttributes } = require("../../models/assets/assets.attributes");
|
||||
@@ -60,6 +61,15 @@ function resolveChecksum(buffer) {
|
||||
return crypto.createHash("sha256").update(buffer).digest("hex");
|
||||
}
|
||||
|
||||
function streamToBuffer(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
stream.on("data", (chunk) => chunks.push(chunk));
|
||||
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
stream.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveResolution(width, height) {
|
||||
if (!width || !height) return null;
|
||||
const h = Math.min(width, height);
|
||||
@@ -524,6 +534,12 @@ exports.streamUploadProgress = (req, res) => {
|
||||
uploadProgress.subscribe(req.params.uploadId, res);
|
||||
};
|
||||
|
||||
// Same broadcaster, reused as-is for document-conversion job progress — it's
|
||||
// just a generic string-keyed SSE channel, nothing upload-specific about it.
|
||||
exports.streamConvertProgress = (req, res) => {
|
||||
uploadProgress.subscribe(req.params.jobId, res);
|
||||
};
|
||||
|
||||
// ─── UPLOAD (batch) ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// Accepts multiple files under the "files" field in one multipart request,
|
||||
@@ -573,6 +589,84 @@ exports.uploadAssetsBatch = async (req, res) => {
|
||||
return R.success(res, `${createdCount} of ${files.length} asset(s) uploaded.`, { results }, 201);
|
||||
};
|
||||
|
||||
// ─── CONVERT TO MARKDOWN ────────────────────────────────────────────────────
|
||||
//
|
||||
// PDF/PPTX -> Markdown, text only (see services/documentConversion.service.js
|
||||
// for the compile/validate/automate stages and why OCR/images are out of
|
||||
// scope). Nothing here is written to the database — the caller (the Lesson
|
||||
// block builder) treats the response as a draft and only persists it if/when
|
||||
// the admin explicitly inserts it into a block and saves the lesson page.
|
||||
//
|
||||
// Mirrors the upload flow's SSE progress pattern exactly: the actual work
|
||||
// runs synchronously inside this request (uploadProgress.service.js is
|
||||
// reused as-is, keyed by a client-generated jobId) while stage transitions
|
||||
// are published for a live "Compiling / Validating / Generating" UI, same as
|
||||
// AddAsset.jsx already renders for uploads.
|
||||
//
|
||||
const CONVERTIBLE_EXTENSIONS = new Set(["pdf", "pptx"]);
|
||||
const MAX_CONVERT_SIZE_BYTES = 25 * 1024 * 1024; // 25MB — keeps this comfortably synchronous
|
||||
// compile() calls the MarkItDown sidecar over HTTP now instead of running
|
||||
// officeparser in-process, so this leaves a bit more room than the original
|
||||
// 45s for network/queueing overhead — MarkItDown's own parsing is plain
|
||||
// CPU-bound work, not ML inference, so it doesn't need much more than that.
|
||||
const CONVERT_TIMEOUT_MS = 60_000;
|
||||
|
||||
exports.convertAssetToMarkdown = async (req, res) => {
|
||||
const { jobId } = req.body;
|
||||
|
||||
try {
|
||||
const { assetId } = req.params;
|
||||
if (!assetId || assetId === "undefined") return R.error(res, "Invalid asset ID.", 400);
|
||||
|
||||
const asset = await Asset.findOne({ where: { asset_id: assetId, ...notDeleted } });
|
||||
if (!asset) return R.error(res, "Asset not found.", 404);
|
||||
|
||||
const extension = (asset.extension || "").toLowerCase();
|
||||
if (!CONVERTIBLE_EXTENSIONS.has(extension)) {
|
||||
return R.error(res, "Only PDF and PPTX documents can be converted to Markdown.", 400);
|
||||
}
|
||||
if (asset.storage_provider !== "s3" || !asset.storage_key) {
|
||||
return R.error(res, "This asset has no stored file to convert.", 400);
|
||||
}
|
||||
if (Number(asset.file_size) > MAX_CONVERT_SIZE_BYTES) {
|
||||
return R.error(res, "File is too large to convert (25MB max).", 400);
|
||||
}
|
||||
|
||||
const publish = (phase) => { if (jobId) uploadProgress.publish(jobId, { phase }); };
|
||||
const abortController = new AbortController();
|
||||
const timeout = setTimeout(() => abortController.abort(), CONVERT_TIMEOUT_MS);
|
||||
|
||||
let markdown, warnings, stats;
|
||||
try {
|
||||
publish("compiling");
|
||||
const { stream } = await s3.getObjectStream(asset.storage_key);
|
||||
const buffer = await streamToBuffer(stream);
|
||||
const ast = await documentConversion.compile(buffer, extension, { signal: abortController.signal });
|
||||
|
||||
publish("validating");
|
||||
const validated = documentConversion.validate(ast);
|
||||
|
||||
publish("generating");
|
||||
const generated = await documentConversion.automate(ast, { signal: abortController.signal });
|
||||
|
||||
markdown = generated.markdown;
|
||||
warnings = [...validated.warnings, ...generated.messages];
|
||||
stats = validated.stats;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
if (jobId) uploadProgress.complete(jobId, { phase: "done" });
|
||||
return R.success(res, "Document converted.", { markdown, warnings, stats });
|
||||
|
||||
} catch (err) {
|
||||
if (jobId) uploadProgress.complete(jobId, { phase: "error", message: err.message });
|
||||
console.error("[ASSET][CONVERT TO MARKDOWN]", err);
|
||||
if (err.status) return R.error(res, err.message, err.status);
|
||||
return R.error(res, "Internal server error.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.updateAsset = async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user