Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -186,7 +186,6 @@ def extract_zip_to_temp(zip_file: UploadFile, password: Optional[str]) -> List[s
|
|
| 186 |
detail=f"Failed to open ZIP file. Check password / integrity. {e}",
|
| 187 |
)
|
| 188 |
|
| 189 |
-
# Only top-level; nested dirs can be added if needed.
|
| 190 |
files = [os.path.join(outdir, f) for f in os.listdir(outdir)]
|
| 191 |
return files
|
| 192 |
|
|
@@ -223,6 +222,35 @@ def health():
|
|
| 223 |
return "OK"
|
| 224 |
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
# ---------- 1. Multi-file transcription (JSON) ----------
|
| 227 |
|
| 228 |
@app.post("/api/transcribe/files", response_model=TranscriptionResponse)
|
|
@@ -505,6 +533,56 @@ HTML_UI = """
|
|
| 505 |
margin-top: -4px;
|
| 506 |
margin-bottom: 8px;
|
| 507 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
@media (max-width: 768px) {
|
| 509 |
header {
|
| 510 |
padding: 12px 16px;
|
|
@@ -518,7 +596,11 @@ HTML_UI = """
|
|
| 518 |
<body>
|
| 519 |
<header>
|
| 520 |
<h1>Whisper Large V3 – Medical Batch Transcription</h1>
|
| 521 |
-
<p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
</header>
|
| 523 |
<main>
|
| 524 |
<div class="card">
|
|
@@ -528,7 +610,9 @@ HTML_UI = """
|
|
| 528 |
<h3>Inputs</h3>
|
| 529 |
<label for="files_input">Audio files</label>
|
| 530 |
<input id="files_input" type="file" multiple accept="audio/*" />
|
| 531 |
-
<div class="small-hint">
|
|
|
|
|
|
|
| 532 |
|
| 533 |
<label for="files_mode">Mode</label>
|
| 534 |
<select id="files_mode">
|
|
@@ -578,10 +662,119 @@ HTML_UI = """
|
|
| 578 |
</div>
|
| 579 |
</div>
|
| 580 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
<div id="status"></div>
|
| 582 |
</main>
|
| 583 |
|
| 584 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
async function postForm(url, formData, expectBlob = false) {
|
| 586 |
const res = await fetch(url, {
|
| 587 |
method: "POST",
|
|
@@ -608,6 +801,27 @@ HTML_UI = """
|
|
| 608 |
document.getElementById("status").innerText = text || "";
|
| 609 |
}
|
| 610 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
// ------- Multi-files JSON -------
|
| 612 |
document.getElementById("btn_files_json").addEventListener("click", async () => {
|
| 613 |
const filesInput = document.getElementById("files_input");
|
|
@@ -627,15 +841,17 @@ HTML_UI = """
|
|
| 627 |
|
| 628 |
setStatus("Transcribing multiple files… (this may take some time for large audio)");
|
| 629 |
out.value = "";
|
|
|
|
| 630 |
|
| 631 |
try {
|
| 632 |
const data = await postForm("/api/transcribe/files", formData, false);
|
| 633 |
out.value = data.combined_transcript || "";
|
| 634 |
setStatus("Done.");
|
|
|
|
| 635 |
} catch (err) {
|
| 636 |
console.error(err);
|
| 637 |
alert(err.message);
|
| 638 |
-
|
| 639 |
}
|
| 640 |
});
|
| 641 |
|
|
@@ -656,6 +872,7 @@ HTML_UI = """
|
|
| 656 |
formData.append("mode", mode);
|
| 657 |
|
| 658 |
setStatus("Generating DOCX for multi-file transcription…");
|
|
|
|
| 659 |
|
| 660 |
try {
|
| 661 |
const blob = await postForm("/api/transcribe/files/docx", formData, true);
|
|
@@ -668,10 +885,11 @@ HTML_UI = """
|
|
| 668 |
a.remove();
|
| 669 |
window.URL.revokeObjectURL(url);
|
| 670 |
setStatus("DOCX downloaded.");
|
|
|
|
| 671 |
} catch (err) {
|
| 672 |
console.error(err);
|
| 673 |
alert(err.message);
|
| 674 |
-
|
| 675 |
}
|
| 676 |
});
|
| 677 |
|
|
@@ -694,15 +912,17 @@ HTML_UI = """
|
|
| 694 |
|
| 695 |
setStatus("Transcribing ZIP contents…");
|
| 696 |
out.value = "";
|
|
|
|
| 697 |
|
| 698 |
try {
|
| 699 |
const data = await postForm("/api/transcribe/zip", formData, false);
|
| 700 |
out.value = data.combined_transcript || "";
|
| 701 |
setStatus("Done.");
|
|
|
|
| 702 |
} catch (err) {
|
| 703 |
console.error(err);
|
| 704 |
alert(err.message);
|
| 705 |
-
|
| 706 |
}
|
| 707 |
});
|
| 708 |
|
|
@@ -723,6 +943,7 @@ HTML_UI = """
|
|
| 723 |
formData.append("mode", mode);
|
| 724 |
|
| 725 |
setStatus("Generating DOCX from ZIP contents…");
|
|
|
|
| 726 |
|
| 727 |
try {
|
| 728 |
const blob = await postForm("/api/transcribe/zip/docx", formData, true);
|
|
@@ -735,12 +956,16 @@ HTML_UI = """
|
|
| 735 |
a.remove();
|
| 736 |
window.URL.revokeObjectURL(url);
|
| 737 |
setStatus("DOCX downloaded.");
|
|
|
|
| 738 |
} catch (err) {
|
| 739 |
console.error(err);
|
| 740 |
alert(err.message);
|
| 741 |
-
|
| 742 |
}
|
| 743 |
});
|
|
|
|
|
|
|
|
|
|
| 744 |
</script>
|
| 745 |
</body>
|
| 746 |
</html>
|
|
|
|
| 186 |
detail=f"Failed to open ZIP file. Check password / integrity. {e}",
|
| 187 |
)
|
| 188 |
|
|
|
|
| 189 |
files = [os.path.join(outdir, f) for f in os.listdir(outdir)]
|
| 190 |
return files
|
| 191 |
|
|
|
|
| 222 |
return "OK"
|
| 223 |
|
| 224 |
|
| 225 |
+
@app.get("/self-test")
|
| 226 |
+
def self_test():
|
| 227 |
+
"""
|
| 228 |
+
Basic self-check:
|
| 229 |
+
- can we create/load the pipeline?
|
| 230 |
+
- what device are we using?
|
| 231 |
+
"""
|
| 232 |
+
try:
|
| 233 |
+
pipe = get_pipeline()
|
| 234 |
+
model_name = getattr(pipe.model, "name_or_path", MODEL_NAME)
|
| 235 |
+
dev = "cuda" if device == 0 else str(device)
|
| 236 |
+
return JSONResponse(
|
| 237 |
+
{
|
| 238 |
+
"status": "ok",
|
| 239 |
+
"message": "Pipeline loaded successfully.",
|
| 240 |
+
"model": model_name,
|
| 241 |
+
"device": dev,
|
| 242 |
+
}
|
| 243 |
+
)
|
| 244 |
+
except Exception as e:
|
| 245 |
+
return JSONResponse(
|
| 246 |
+
{
|
| 247 |
+
"status": "error",
|
| 248 |
+
"message": f"Pipeline failed to load: {e}",
|
| 249 |
+
},
|
| 250 |
+
status_code=500,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
# ---------- 1. Multi-file transcription (JSON) ----------
|
| 255 |
|
| 256 |
@app.post("/api/transcribe/files", response_model=TranscriptionResponse)
|
|
|
|
| 533 |
margin-top: -4px;
|
| 534 |
margin-bottom: 8px;
|
| 535 |
}
|
| 536 |
+
code {
|
| 537 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 538 |
+
font-size: 12px;
|
| 539 |
+
}
|
| 540 |
+
pre {
|
| 541 |
+
background: #0b1120;
|
| 542 |
+
color: #e5e7eb;
|
| 543 |
+
padding: 10px 12px;
|
| 544 |
+
border-radius: 10px;
|
| 545 |
+
overflow-x: auto;
|
| 546 |
+
font-size: 12px;
|
| 547 |
+
line-height: 1.5;
|
| 548 |
+
}
|
| 549 |
+
a {
|
| 550 |
+
color: #1d4ed8;
|
| 551 |
+
text-decoration: none;
|
| 552 |
+
}
|
| 553 |
+
a:hover {
|
| 554 |
+
text-decoration: underline;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
/* Progress bar */
|
| 558 |
+
#progress-wrapper {
|
| 559 |
+
margin-top: 10px;
|
| 560 |
+
margin-bottom: 4px;
|
| 561 |
+
font-size: 12px;
|
| 562 |
+
color: #4b5563;
|
| 563 |
+
}
|
| 564 |
+
#progress-track {
|
| 565 |
+
width: 100%;
|
| 566 |
+
height: 8px;
|
| 567 |
+
background: #e5e7eb;
|
| 568 |
+
border-radius: 999px;
|
| 569 |
+
overflow: hidden;
|
| 570 |
+
margin-top: 4px;
|
| 571 |
+
}
|
| 572 |
+
#progress-fill {
|
| 573 |
+
height: 100%;
|
| 574 |
+
width: 0%;
|
| 575 |
+
background: #111827;
|
| 576 |
+
border-radius: 999px;
|
| 577 |
+
transition: width 0.2s ease-out;
|
| 578 |
+
}
|
| 579 |
+
#progress-text {
|
| 580 |
+
font-size: 11px;
|
| 581 |
+
color: #6b7280;
|
| 582 |
+
margin-top: 3px;
|
| 583 |
+
min-height: 14px;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
@media (max-width: 768px) {
|
| 587 |
header {
|
| 588 |
padding: 12px 16px;
|
|
|
|
| 596 |
<body>
|
| 597 |
<header>
|
| 598 |
<h1>Whisper Large V3 – Medical Batch Transcription</h1>
|
| 599 |
+
<p>
|
| 600 |
+
Upload multiple audio files or a password-protected ZIP.
|
| 601 |
+
Mode: <code>general</code> or <code>medical_en</code>.
|
| 602 |
+
API docs at <code>/docs</code>.
|
| 603 |
+
</p>
|
| 604 |
</header>
|
| 605 |
<main>
|
| 606 |
<div class="card">
|
|
|
|
| 610 |
<h3>Inputs</h3>
|
| 611 |
<label for="files_input">Audio files</label>
|
| 612 |
<input id="files_input" type="file" multiple accept="audio/*" />
|
| 613 |
+
<div class="small-hint">
|
| 614 |
+
You can select multiple audio files.
|
| 615 |
+
</div>
|
| 616 |
|
| 617 |
<label for="files_mode">Mode</label>
|
| 618 |
<select id="files_mode">
|
|
|
|
| 662 |
</div>
|
| 663 |
</div>
|
| 664 |
|
| 665 |
+
<!-- 3. Quick examples -->
|
| 666 |
+
<div class="card">
|
| 667 |
+
<h2>3. Quick examples <span class="pill">API & sample audio</span></h2>
|
| 668 |
+
<h3>Sample audio for testing (download & upload above)</h3>
|
| 669 |
+
<p class="small-hint">
|
| 670 |
+
1. Download this small public sample file<br>
|
| 671 |
+
2. Upload it in section 1 and click <strong>Transcribe → JSON</strong>
|
| 672 |
+
</p>
|
| 673 |
+
<p>
|
| 674 |
+
👉 <a href="https://huggingface.co/datasets/Narsil/asr_dummy/resolve/main/mlk.flac" target="_blank" rel="noopener">
|
| 675 |
+
Download example audio (mlk.flac)
|
| 676 |
+
</a>
|
| 677 |
+
</p>
|
| 678 |
+
|
| 679 |
+
<h3>Example: cURL for multi-file JSON</h3>
|
| 680 |
+
<p class="small-hint">Replace <code>@path/to/audio1.flac</code> with your local file path.</p>
|
| 681 |
+
<pre><code>curl -X POST \\
|
| 682 |
+
"https://staraks-whisper-large-v3.hf.space/api/transcribe/files" \\
|
| 683 |
+
-H "Accept: application/json" \\
|
| 684 |
+
-F "mode=medical_en" \\
|
| 685 |
+
-F "files=@path/to/audio1.flac" \\
|
| 686 |
+
-F "files=@path/to/audio2.wav"</code></pre>
|
| 687 |
+
|
| 688 |
+
<h3>Example: cURL for ZIP JSON</h3>
|
| 689 |
+
<p class="small-hint">ZIP file contains multiple audio files. Password field is optional.</p>
|
| 690 |
+
<pre><code>curl -X POST \\
|
| 691 |
+
"https://staraks-whisper-large-v3.hf.space/api/transcribe/zip" \\
|
| 692 |
+
-H "Accept: application/json" \\
|
| 693 |
+
-F "mode=medical_en" \\
|
| 694 |
+
-F "file=@path/to/audios.zip" \\
|
| 695 |
+
-F "password="</code></pre>
|
| 696 |
+
</div>
|
| 697 |
+
|
| 698 |
+
<!-- 4. System self-check -->
|
| 699 |
+
<div class="card">
|
| 700 |
+
<h2>4. System self-check <span class="pill">Model & API status</span></h2>
|
| 701 |
+
<p class="small-hint">
|
| 702 |
+
Use this to quickly verify that the API is running and the Whisper pipeline can be loaded.
|
| 703 |
+
</p>
|
| 704 |
+
<button class="btn-primary" id="btn_self_test">Run self-test</button>
|
| 705 |
+
<pre id="self_test_output"><code>Click "Run self-test" to see status...</code></pre>
|
| 706 |
+
</div>
|
| 707 |
+
|
| 708 |
+
<!-- Progress & status -->
|
| 709 |
+
<div id="progress-wrapper">
|
| 710 |
+
<div>Transcription progress</div>
|
| 711 |
+
<div id="progress-track">
|
| 712 |
+
<div id="progress-fill"></div>
|
| 713 |
+
</div>
|
| 714 |
+
<div id="progress-text">Idle</div>
|
| 715 |
+
</div>
|
| 716 |
<div id="status"></div>
|
| 717 |
</main>
|
| 718 |
|
| 719 |
<script>
|
| 720 |
+
let __progressTimer = null;
|
| 721 |
+
let __progressValue = 0;
|
| 722 |
+
|
| 723 |
+
function setProgress(value, label) {
|
| 724 |
+
__progressValue = Math.max(0, Math.min(100, value));
|
| 725 |
+
const fill = document.getElementById("progress-fill");
|
| 726 |
+
const text = document.getElementById("progress-text");
|
| 727 |
+
if (fill) {
|
| 728 |
+
fill.style.width = __progressValue + "%";
|
| 729 |
+
}
|
| 730 |
+
if (text) {
|
| 731 |
+
text.innerText = label + " (" + __progressValue.toFixed(0) + "%)";
|
| 732 |
+
}
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
function resetProgress() {
|
| 736 |
+
if (__progressTimer) {
|
| 737 |
+
clearInterval(__progressTimer);
|
| 738 |
+
__progressTimer = null;
|
| 739 |
+
}
|
| 740 |
+
setProgress(0, "Idle");
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
function startSimulatedProgress(label) {
|
| 744 |
+
if (__progressTimer) {
|
| 745 |
+
clearInterval(__progressTimer);
|
| 746 |
+
}
|
| 747 |
+
let p = 5;
|
| 748 |
+
setProgress(p, label);
|
| 749 |
+
__progressTimer = setInterval(() => {
|
| 750 |
+
if (p < 90) {
|
| 751 |
+
p += Math.random() * 10;
|
| 752 |
+
if (p > 90) p = 90;
|
| 753 |
+
setProgress(p, label);
|
| 754 |
+
}
|
| 755 |
+
}, 600);
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
function finishProgress(label) {
|
| 759 |
+
if (__progressTimer) {
|
| 760 |
+
clearInterval(__progressTimer);
|
| 761 |
+
__progressTimer = null;
|
| 762 |
+
}
|
| 763 |
+
setProgress(100, label);
|
| 764 |
+
setTimeout(() => {
|
| 765 |
+
resetProgress();
|
| 766 |
+
}, 2000);
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
function errorProgress(message) {
|
| 770 |
+
if (__progressTimer) {
|
| 771 |
+
clearInterval(__progressTimer);
|
| 772 |
+
__progressTimer = null;
|
| 773 |
+
}
|
| 774 |
+
setProgress(0, "Error");
|
| 775 |
+
setStatus(message);
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
async function postForm(url, formData, expectBlob = false) {
|
| 779 |
const res = await fetch(url, {
|
| 780 |
method: "POST",
|
|
|
|
| 801 |
document.getElementById("status").innerText = text || "";
|
| 802 |
}
|
| 803 |
|
| 804 |
+
// ------- Self test -------
|
| 805 |
+
document.getElementById("btn_self_test").addEventListener("click", async () => {
|
| 806 |
+
const out = document.getElementById("self_test_output");
|
| 807 |
+
out.textContent = "Running self-test...";
|
| 808 |
+
setStatus("Running self-test…");
|
| 809 |
+
try {
|
| 810 |
+
const res = await fetch("/self-test");
|
| 811 |
+
const data = await res.json();
|
| 812 |
+
out.textContent = JSON.stringify(data, null, 2);
|
| 813 |
+
if (data.status === "ok") {
|
| 814 |
+
setStatus("Self-test OK – model and API are working.");
|
| 815 |
+
} else {
|
| 816 |
+
setStatus("Self-test reported an error.");
|
| 817 |
+
}
|
| 818 |
+
} catch (err) {
|
| 819 |
+
console.error(err);
|
| 820 |
+
out.textContent = "Self-test failed: " + err.message;
|
| 821 |
+
setStatus("Self-test failed.");
|
| 822 |
+
}
|
| 823 |
+
});
|
| 824 |
+
|
| 825 |
// ------- Multi-files JSON -------
|
| 826 |
document.getElementById("btn_files_json").addEventListener("click", async () => {
|
| 827 |
const filesInput = document.getElementById("files_input");
|
|
|
|
| 841 |
|
| 842 |
setStatus("Transcribing multiple files… (this may take some time for large audio)");
|
| 843 |
out.value = "";
|
| 844 |
+
startSimulatedProgress("Transcribing files");
|
| 845 |
|
| 846 |
try {
|
| 847 |
const data = await postForm("/api/transcribe/files", formData, false);
|
| 848 |
out.value = data.combined_transcript || "";
|
| 849 |
setStatus("Done.");
|
| 850 |
+
finishProgress("Transcription complete");
|
| 851 |
} catch (err) {
|
| 852 |
console.error(err);
|
| 853 |
alert(err.message);
|
| 854 |
+
errorProgress("Error during transcription.");
|
| 855 |
}
|
| 856 |
});
|
| 857 |
|
|
|
|
| 872 |
formData.append("mode", mode);
|
| 873 |
|
| 874 |
setStatus("Generating DOCX for multi-file transcription…");
|
| 875 |
+
startSimulatedProgress("Generating DOCX");
|
| 876 |
|
| 877 |
try {
|
| 878 |
const blob = await postForm("/api/transcribe/files/docx", formData, true);
|
|
|
|
| 885 |
a.remove();
|
| 886 |
window.URL.revokeObjectURL(url);
|
| 887 |
setStatus("DOCX downloaded.");
|
| 888 |
+
finishProgress("DOCX ready");
|
| 889 |
} catch (err) {
|
| 890 |
console.error(err);
|
| 891 |
alert(err.message);
|
| 892 |
+
errorProgress("Error during DOCX generation.");
|
| 893 |
}
|
| 894 |
});
|
| 895 |
|
|
|
|
| 912 |
|
| 913 |
setStatus("Transcribing ZIP contents…");
|
| 914 |
out.value = "";
|
| 915 |
+
startSimulatedProgress("Transcribing ZIP");
|
| 916 |
|
| 917 |
try {
|
| 918 |
const data = await postForm("/api/transcribe/zip", formData, false);
|
| 919 |
out.value = data.combined_transcript || "";
|
| 920 |
setStatus("Done.");
|
| 921 |
+
finishProgress("ZIP transcription complete");
|
| 922 |
} catch (err) {
|
| 923 |
console.error(err);
|
| 924 |
alert(err.message);
|
| 925 |
+
errorProgress("Error during ZIP transcription.");
|
| 926 |
}
|
| 927 |
});
|
| 928 |
|
|
|
|
| 943 |
formData.append("mode", mode);
|
| 944 |
|
| 945 |
setStatus("Generating DOCX from ZIP contents…");
|
| 946 |
+
startSimulatedProgress("Generating ZIP DOCX");
|
| 947 |
|
| 948 |
try {
|
| 949 |
const blob = await postForm("/api/transcribe/zip/docx", formData, true);
|
|
|
|
| 956 |
a.remove();
|
| 957 |
window.URL.revokeObjectURL(url);
|
| 958 |
setStatus("DOCX downloaded.");
|
| 959 |
+
finishProgress("ZIP DOCX ready");
|
| 960 |
} catch (err) {
|
| 961 |
console.error(err);
|
| 962 |
alert(err.message);
|
| 963 |
+
errorProgress("Error during ZIP DOCX generation.");
|
| 964 |
}
|
| 965 |
});
|
| 966 |
+
|
| 967 |
+
// Initial state
|
| 968 |
+
resetProgress();
|
| 969 |
</script>
|
| 970 |
</body>
|
| 971 |
</html>
|