cstr commited on
Commit
26bc8a4
Β·
verified Β·
1 Parent(s): 1799c9b

Upload 2 files

Browse files
Files changed (2) hide show
  1. format_transplant.py +0 -0
  2. transplant-app.py +589 -0
format_transplant.py ADDED
The diff for this file is too large to render. See raw diff
 
transplant-app.py ADDED
@@ -0,0 +1,589 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gradio interface for Format Transplant
4
+ Designed for local use and Hugging Face Spaces deployment.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ import sys
10
+ import tempfile
11
+ from pathlib import Path
12
+ from typing import Optional, Tuple, List
13
+
14
+ import gradio as gr
15
+
16
+ # ============================================================================
17
+ # LOGGING
18
+ # ============================================================================
19
+
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format="%(asctime)s [%(levelname)-7s] %(message)s",
23
+ datefmt="%H:%M:%S",
24
+ )
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # ============================================================================
28
+ # IMPORT CORE ENGINE
29
+ # ============================================================================
30
+
31
+ try:
32
+ from format_transplant import (
33
+ FormatTransplanter,
34
+ LLMFormatTransplanter,
35
+ PROVIDER_DEFAULTS,
36
+ llm_config_from_args,
37
+ )
38
+ ENGINE_OK = True
39
+ ENGINE_ERROR = None
40
+ except Exception as _e:
41
+ ENGINE_OK = False
42
+ ENGINE_ERROR = str(_e)
43
+ logger.error("Failed to import format_transplant: %s", _e)
44
+
45
+ # ============================================================================
46
+ # ENVIRONMENT STATUS
47
+ # ============================================================================
48
+
49
+ def _check_environment() -> str:
50
+ lines = []
51
+
52
+ def _ok(msg): lines.append(f"βœ“ {msg}")
53
+ def _err(msg): lines.append(f"βœ— {msg}")
54
+ def _inf(msg): lines.append(f"β„Ή {msg}")
55
+
56
+ # python-docx
57
+ try:
58
+ from docx import Document
59
+ _ok("python-docx installed")
60
+ except ImportError:
61
+ _err("python-docx missing – run: pip install python-docx")
62
+
63
+ # lxml
64
+ try:
65
+ from lxml import etree
66
+ _ok("lxml installed")
67
+ except ImportError:
68
+ _err("lxml missing – run: pip install lxml")
69
+
70
+ # Core engine
71
+ if ENGINE_OK:
72
+ _ok("format_transplant engine loaded")
73
+ else:
74
+ _err(f"format_transplant engine failed to load: {ENGINE_ERROR}")
75
+
76
+ lines.append("")
77
+ lines.append("── LLM providers ──")
78
+
79
+ # openai (covers OpenAI / Nebius / Scaleway / OpenRouter / Mistral)
80
+ try:
81
+ import openai
82
+ _ok(f"openai SDK {openai.__version__} (covers OpenAI, Nebius, Scaleway, OpenRouter, Mistral)")
83
+ except ImportError:
84
+ _inf("openai SDK missing – run: pip install openai (needed for OpenAI/Nebius/Scaleway/OpenRouter/Mistral)")
85
+
86
+ # anthropic
87
+ try:
88
+ import anthropic
89
+ _ok(f"anthropic SDK {anthropic.__version__}")
90
+ except ImportError:
91
+ _inf("anthropic SDK missing – run: pip install anthropic")
92
+
93
+ # fastapi-poe
94
+ try:
95
+ import fastapi_poe
96
+ _ok("fastapi-poe installed")
97
+ except ImportError:
98
+ _inf("fastapi-poe missing – run: pip install fastapi-poe (needed for Poe)")
99
+
100
+ lines.append("")
101
+ lines.append("── Detected API keys ──")
102
+ for provider, defaults in (PROVIDER_DEFAULTS.items() if ENGINE_OK else {}.items()):
103
+ env = defaults.get("env", "")
104
+ if env and os.getenv(env):
105
+ _ok(f"{provider.capitalize()} key found ({env})")
106
+
107
+ return "\n".join(lines)
108
+
109
+
110
+ SETUP_STATUS = _check_environment()
111
+ logger.info("Environment status:\n%s", SETUP_STATUS)
112
+
113
+ # ============================================================================
114
+ # CORE PROCESSING FUNCTION
115
+ # ============================================================================
116
+
117
+ def run_transplant(
118
+ blueprint_file: Optional[str],
119
+ source_file: Optional[str],
120
+ style_overrides_text: str,
121
+ verbose: bool,
122
+ # LLM parameters
123
+ llm_provider: str,
124
+ llm_model: str,
125
+ llm_api_key: str,
126
+ llm_mode: str,
127
+ styleguide_in_file: Optional[str],
128
+ extra_styleguide_files, # list[str] | None from gr.File(file_count="multiple")
129
+ llm_batch_size: int,
130
+ llm_context_chars: int,
131
+ progress=gr.Progress(),
132
+ ) -> Tuple[Optional[str], Optional[str], str]:
133
+ """
134
+ Main handler called by the Gradio button.
135
+
136
+ Returns (output_docx_path_or_None, styleguide_path_or_None, log_string).
137
+ """
138
+
139
+ use_llm = bool(llm_provider and llm_provider != "(none)")
140
+
141
+ # ── Validate inputs ───────────────────────────────────────────────
142
+ if blueprint_file is None:
143
+ return None, None, "❌ No blueprint file uploaded."
144
+ if source_file is None and llm_mode != "styleguide_only":
145
+ return None, None, "❌ No source file uploaded."
146
+
147
+ blueprint_path = Path(blueprint_file)
148
+ source_path = Path(source_file) if source_file else None
149
+
150
+ if blueprint_path.suffix.lower() != ".docx":
151
+ return None, None, "❌ Blueprint must be a .docx file."
152
+ if source_path and source_path.suffix.lower() != ".docx":
153
+ return None, None, "❌ Source must be a .docx file."
154
+
155
+ if not ENGINE_OK:
156
+ return None, None, f"❌ Engine not available: {ENGINE_ERROR}"
157
+
158
+ # ── Parse style overrides ─────────────────────────────────────────
159
+ overrides = {}
160
+ if style_overrides_text.strip():
161
+ for raw_line in style_overrides_text.strip().splitlines():
162
+ line = raw_line.strip()
163
+ if not line or line.startswith("#"):
164
+ continue
165
+ if "=" not in line:
166
+ logger.warning("Ignored override line (no '='): '%s'", line)
167
+ continue
168
+ src, _, bp = line.partition("=")
169
+ overrides[src.strip()] = bp.strip()
170
+
171
+ # ── Output paths ───────────────────────────────────────────────────
172
+ temp_dir = Path(tempfile.mkdtemp())
173
+ output_filename = f"{source_path.stem}_transplanted.docx" if source_path else "transplanted.docx"
174
+ output_path = temp_dir / output_filename
175
+ styleguide_out_path = temp_dir / "styleguide.md"
176
+
177
+ # ── Log capture ───────────────────────────────────────────────────
178
+ log_records: list = []
179
+
180
+ class _Capture(logging.Handler):
181
+ def emit(self, record):
182
+ log_records.append(self.format(record))
183
+
184
+ capture_handler = _Capture()
185
+ capture_handler.setFormatter(logging.Formatter("[%(levelname)-7s] %(message)s"))
186
+
187
+ root_log = logging.getLogger()
188
+ saved_level = root_log.level
189
+ root_log.addHandler(capture_handler)
190
+ root_log.setLevel(logging.DEBUG if verbose else logging.INFO)
191
+
192
+ saved_sg_path: Optional[str] = None
193
+
194
+ try:
195
+ progress(0.05, desc="Checking files…")
196
+
197
+ if use_llm:
198
+ # ── Build LLM config ──────────────────────────────────────
199
+ progress(0.10, desc="Initialising LLM client…")
200
+
201
+ llm_cfg = llm_config_from_args(
202
+ provider_str=llm_provider,
203
+ model=llm_model.strip() or None,
204
+ api_key=llm_api_key.strip() or None,
205
+ )
206
+ llm_cfg.para_batch_size = int(llm_batch_size)
207
+ llm_cfg.blueprint_context_chars = int(llm_context_chars)
208
+
209
+ extra_sg_paths: Optional[List[Path]] = None
210
+ if extra_styleguide_files:
211
+ files = extra_styleguide_files if isinstance(extra_styleguide_files, list) else [extra_styleguide_files]
212
+ extra_sg_paths = [Path(f) for f in files if f]
213
+
214
+ sg_in: Optional[Path] = None
215
+ if styleguide_in_file:
216
+ sg_in = Path(styleguide_in_file)
217
+
218
+ transplanter = LLMFormatTransplanter()
219
+
220
+ progress(0.15, desc="Phase 1 – Analysing blueprint…")
221
+ progress(0.25, desc="Phase 1-LLM – Generating style guide…")
222
+ progress(0.45, desc="Phase 2 – Extracting & LLM-formatting content…")
223
+ progress(0.70, desc="Phase 3-4 – Style mapping & document assembly…")
224
+
225
+ saved_sg = transplanter.run(
226
+ blueprint_path=blueprint_path,
227
+ source_path=source_path,
228
+ output_path=output_path if llm_mode != "styleguide_only" else temp_dir / "_unused.docx",
229
+ llm_config=llm_cfg,
230
+ extra_styleguide_paths=extra_sg_paths,
231
+ styleguide_in=sg_in,
232
+ styleguide_out=styleguide_out_path,
233
+ llm_mode=llm_mode,
234
+ user_style_overrides=overrides or None,
235
+ )
236
+
237
+ if saved_sg and saved_sg.exists():
238
+ saved_sg_path = str(saved_sg)
239
+
240
+ else:
241
+ # ── Structural transplant only (no LLM) ───────────────────
242
+ transplanter = FormatTransplanter()
243
+
244
+ progress(0.15, desc="Phase 1 – Analysing blueprint…")
245
+ progress(0.30, desc="Phase 2 – Extracting source content…")
246
+ progress(0.55, desc="Phase 3 – Mapping styles…")
247
+ progress(0.70, desc="Phase 4 – Building output document…")
248
+
249
+ transplanter.run(
250
+ blueprint_path=blueprint_path,
251
+ source_path=source_path,
252
+ output_path=output_path,
253
+ user_style_overrides=overrides or None,
254
+ )
255
+
256
+ progress(1.0, desc="βœ“ Complete!")
257
+
258
+ except Exception as exc:
259
+ root_log.removeHandler(capture_handler)
260
+ root_log.setLevel(saved_level)
261
+ log_text = "\n".join(log_records)
262
+ logger.error("Transplant failed: %s", exc, exc_info=True)
263
+ return None, None, (
264
+ f"❌ Error: {exc}\n\n"
265
+ f"── Log before error ──\n{log_text}"
266
+ )
267
+
268
+ root_log.removeHandler(capture_handler)
269
+ root_log.setLevel(saved_level)
270
+
271
+ # ── Build summary ──────────────���──────────────────────────────────
272
+ log_text = "\n".join(log_records)
273
+
274
+ mapper_lines = [l for l in log_records if "[MAPPER]" in l and "β†’" in l]
275
+ mapper_summary = "\n".join(mapper_lines) if mapper_lines else "(none)"
276
+
277
+ llm_lines = [l for l in log_records if "Phase 1-LLM" in l or "Phase 2-LLM" in l]
278
+ llm_summary = "\n".join(llm_lines) if llm_lines else ""
279
+
280
+ out_filename = output_filename if llm_mode != "styleguide_only" else "(none – styleguide_only mode)"
281
+ out_path_for_return = str(output_path) if (llm_mode != "styleguide_only" and output_path.exists()) else None
282
+
283
+ summary_parts = [
284
+ "βœ… Format Transplant Complete!\n",
285
+ f"πŸ“‹ Blueprint : {blueprint_path.name}",
286
+ f"πŸ“„ Source : {source_path.name if source_path else '(none)'}",
287
+ f"πŸ“€ Output : {out_filename}",
288
+ f"🎨 Overrides : {len(overrides)}",
289
+ ]
290
+ if use_llm:
291
+ summary_parts += [
292
+ f"πŸ€– LLM : {llm_provider} / {llm_cfg.model}",
293
+ f"πŸ”§ Mode : {llm_mode}",
294
+ ]
295
+ if llm_summary:
296
+ summary_parts += ["\n── LLM phases ──", llm_summary]
297
+ summary_parts += [
298
+ "\n── Style mapping ──",
299
+ mapper_summary,
300
+ "\n── Full log ──",
301
+ log_text,
302
+ ]
303
+
304
+ return out_path_for_return, saved_sg_path, "\n".join(summary_parts)
305
+
306
+
307
+ # ============================================================================
308
+ # GRADIO INTERFACE
309
+ # ============================================================================
310
+
311
+ _PROVIDER_CHOICES = ["(none)"] + list(PROVIDER_DEFAULTS.keys()) if ENGINE_OK else ["(none)"]
312
+
313
+
314
+ def _default_model_for_provider(provider: str) -> str:
315
+ """Return the default model string for a provider name."""
316
+ if not ENGINE_OK or provider == "(none)":
317
+ return ""
318
+ return PROVIDER_DEFAULTS.get(provider, {}).get("model", "")
319
+
320
+
321
+ def create_interface() -> gr.Blocks:
322
+
323
+ with gr.Blocks(title="Format Transplant") as demo:
324
+
325
+ gr.Markdown("""
326
+ # 🎨 Format Transplant
327
+
328
+ Apply the **complete formatting** of a blueprint document to the **content** of a source document β€” down to paragraph styles, page layout, margins, headers, footers, and footnotes.
329
+ Optionally run an **LLM style pass** that learns editorial conventions from the blueprint and re-formats source paragraphs and footnotes accordingly.
330
+
331
+ | What comes from the **blueprint** | What comes from the **source** |
332
+ |---|---|
333
+ | Page size, margins, section layout | All body text |
334
+ | All style definitions (fonts, indents, spacing) | Bold / italic / underline of runs |
335
+ | Headers & footers | Tables (with remapped styles) |
336
+ | Footnote formatting | Footnote text content |
337
+ """)
338
+
339
+ with gr.Row():
340
+
341
+ # ── Left column: inputs ────────────────────────────────────
342
+ with gr.Column(scale=1):
343
+ gr.Markdown("### πŸ“‹ Input files")
344
+
345
+ blueprint_file = gr.File(
346
+ label="β‘  Blueprint DOCX (provides formatting)",
347
+ file_types=[".docx"],
348
+ type="filepath",
349
+ )
350
+
351
+ source_file = gr.File(
352
+ label="β‘‘ Source DOCX (provides content)",
353
+ file_types=[".docx"],
354
+ type="filepath",
355
+ )
356
+
357
+ gr.Markdown("### βš™οΈ Options")
358
+
359
+ style_overrides = gr.Textbox(
360
+ label="Style overrides (optional)",
361
+ placeholder=(
362
+ "One mapping per line:\n"
363
+ " Source Style Name = Blueprint Style Name\n\n"
364
+ "Examples:\n"
365
+ " My Custom Body = Normal\n"
366
+ " Big Header = Heading 1\n"
367
+ " Zitat = Intense Quote"
368
+ ),
369
+ lines=5,
370
+ info=(
371
+ "Leave blank for automatic mapping. "
372
+ "Check the log for [MAPPER] lines to audit what was resolved."
373
+ ),
374
+ )
375
+
376
+ verbose = gr.Checkbox(
377
+ label="Verbose debug logging",
378
+ value=False,
379
+ info="Shows every XML element stripped/reset β€” helpful for troubleshooting.",
380
+ )
381
+
382
+ run_btn = gr.Button(
383
+ "πŸš€ Run Format Transplant",
384
+ variant="primary",
385
+ size="lg",
386
+ )
387
+
388
+ # ── Right column: outputs ──────────────────────────────────
389
+ with gr.Column(scale=1):
390
+ gr.Markdown("### πŸ“₯ Result")
391
+
392
+ output_file = gr.File(
393
+ label="Transplanted document (.docx)",
394
+ interactive=False,
395
+ )
396
+
397
+ styleguide_file = gr.File(
398
+ label="Generated style guide (.md) β€” only produced when LLM is enabled",
399
+ interactive=False,
400
+ )
401
+
402
+ log_output = gr.Textbox(
403
+ label="Log",
404
+ lines=24,
405
+ max_lines=60,
406
+ interactive=False,
407
+ )
408
+
409
+ # ── LLM accordion ──────────────────────────────────────────────
410
+ with gr.Accordion("πŸ€– LLM Style Pass (optional)", open=False):
411
+ gr.Markdown("""
412
+ Select an LLM provider to add an **editorial style pass** on top of the structural format transplant:
413
+
414
+ 1. **Style guide generation** β€” the LLM reads a sample of the blueprint and produces a `styleguide.md`
415
+ describing conventions like: _names always italic, DMG transliteration for Arabic, citation markers,
416
+ foreign terms in italics, quotation-mark style_, etc.
417
+ 2. **Content formatting** β€” each source paragraph / footnote is sent (in batches) to the LLM together
418
+ with the style guide. The LLM returns Markdown with bold/italic applied; these runs replace the
419
+ original runs in the transplanted document.
420
+
421
+ Leave **Provider** at `(none)` to skip the LLM pass entirely (fast, structural-only transplant).
422
+ """)
423
+
424
+ with gr.Row():
425
+ llm_provider = gr.Dropdown(
426
+ label="Provider",
427
+ choices=_PROVIDER_CHOICES,
428
+ value="(none)",
429
+ info="Select an LLM provider. API key must be available.",
430
+ )
431
+ llm_model = gr.Textbox(
432
+ label="Model (leave blank for provider default)",
433
+ placeholder="auto",
434
+ info="e.g. gpt-4o, claude-opus-4-5, mistral-large-latest, …",
435
+ )
436
+ llm_api_key = gr.Textbox(
437
+ label="API key (leave blank to use env var)",
438
+ type="password",
439
+ placeholder="sk-…",
440
+ info="Falls back to the provider's env variable if empty.",
441
+ )
442
+
443
+ with gr.Row():
444
+ llm_mode = gr.Radio(
445
+ label="LLM mode",
446
+ choices=["both", "paragraphs", "footnotes", "styleguide_only"],
447
+ value="both",
448
+ info=(
449
+ "both β€” format paragraphs and footnotes | "
450
+ "styleguide_only β€” only generate the style guide, no output document"
451
+ ),
452
+ )
453
+
454
+ with gr.Row():
455
+ llm_batch_size = gr.Slider(
456
+ label="Batch size (paragraphs per LLM call)",
457
+ minimum=1,
458
+ maximum=50,
459
+ step=1,
460
+ value=15,
461
+ info="Smaller = more calls, larger = may hit context limits.",
462
+ )
463
+ llm_context_chars = gr.Slider(
464
+ label="Blueprint context (chars sent for style guide generation)",
465
+ minimum=5_000,
466
+ maximum=120_000,
467
+ step=5_000,
468
+ value=40_000,
469
+ info="~4 chars β‰ˆ 1 token. Adjust to fit your model's context window.",
470
+ )
471
+
472
+ with gr.Row():
473
+ styleguide_in = gr.File(
474
+ label="Pre-existing style guide (.md) β€” skip generation if provided",
475
+ file_types=[".md", ".txt"],
476
+ type="filepath",
477
+ )
478
+ extra_styleguides = gr.File(
479
+ label="Extra style guide files (optional, multiple)",
480
+ file_types=[".md", ".txt", ".pdf"],
481
+ type="filepath",
482
+ file_count="multiple",
483
+ )
484
+
485
+ # Auto-fill default model when provider changes
486
+ def _on_provider_change(provider):
487
+ return _default_model_for_provider(provider)
488
+
489
+ llm_provider.change(
490
+ fn=_on_provider_change,
491
+ inputs=[llm_provider],
492
+ outputs=[llm_model],
493
+ )
494
+
495
+ # ── System status ──────────────────────────────────────────────
496
+ with gr.Accordion("System status", open=False):
497
+ gr.Markdown(f"```\n{SETUP_STATUS}\n```")
498
+
499
+ # ── Help / docs ────────────────────────────────────────────────
500
+ with gr.Accordion("How it works", open=False):
501
+ gr.Markdown("""
502
+ ### Structural pipeline (always runs)
503
+
504
+ 1. **Blueprint analysis** β€” reads every section (margins, page size, header/footer distance) and
505
+ every style definition (font, size, bold, italic, indents) from the blueprint, resolving the
506
+ full style inheritance chain.
507
+
508
+ 2. **Content extraction** β€” pulls all body paragraphs and tables from the source in order,
509
+ capturing text and semantic inline formatting (bold/italic/underline).
510
+ Footnote content is also extracted.
511
+
512
+ 3. **Style mapping** β€” each source paragraph style is mapped to the best blueprint style:
513
+ exact name β†’ case-insensitive β†’ semantic class β†’ fallback to Normal.
514
+ Semantic classes include headings 1–9 (detected in DE/FR/IT/ES/RU/ZH/PL/SE/EN),
515
+ footnote text, captions, block-quotes, abstracts.
516
+
517
+ 4. **Document assembly** β€” a copy of the blueprint becomes the output.
518
+ Its body is cleared (the final `<w:sectPr>` that holds page layout is kept).
519
+ Source elements are inserted one by one with the mapped style applied.
520
+ Footnotes are transplanted with the blueprint's footnote text style.
521
+
522
+ ### LLM style pass (when provider β‰  none)
523
+
524
+ **Phase 1-LLM** β€” the LLM receives a sample of the blueprint text and produces a `styleguide.md`
525
+ with self-instructions about editorial conventions. You can skip this by uploading a pre-existing
526
+ style guide (the file input above), or you can run `styleguide_only` mode first, inspect the result,
527
+ edit it, then re-run with the edited file as input.
528
+
529
+ **Phase 2-LLM** β€” source paragraphs and footnotes are sent in batches to the LLM together with the
530
+ style guide. The LLM returns `***bold+italic***` / `**bold**` / `*italic*` Markdown. These inline
531
+ markers are parsed and the resulting runs replace the original content in the transplanted document.
532
+
533
+ ### Style override syntax
534
+
535
+ One mapping per line in the **Style overrides** box:
536
+
537
+ ```
538
+ Source Style Name = Blueprint Style Name
539
+ My Custom Body = Normal
540
+ Big Chapter Head = Heading 1
541
+ Blockzitat = Intense Quote
542
+ ```
543
+
544
+ ### Debug tips
545
+
546
+ - Enable **Verbose** logging and read the full log.
547
+ - `[MAPPER]` lines show every style resolution and why.
548
+ - `[BUILD]` lines show every element inserted into the output.
549
+ - `[BLUEPRINT]` lines show what was read from the blueprint.
550
+ - `[EXTRACT]` lines show what was read from the source.
551
+
552
+ If a style isn't mapping correctly, copy its exact name from a `[EXTRACT]` line
553
+ and add an override.
554
+ """)
555
+
556
+ # ── Wire button ────────────────────────────────────────────────
557
+ run_btn.click(
558
+ fn=run_transplant,
559
+ inputs=[
560
+ blueprint_file,
561
+ source_file,
562
+ style_overrides,
563
+ verbose,
564
+ llm_provider,
565
+ llm_model,
566
+ llm_api_key,
567
+ llm_mode,
568
+ styleguide_in,
569
+ extra_styleguides,
570
+ llm_batch_size,
571
+ llm_context_chars,
572
+ ],
573
+ outputs=[output_file, styleguide_file, log_output],
574
+ )
575
+
576
+ return demo
577
+
578
+
579
+ # ============================================================================
580
+ # MAIN
581
+ # ============================================================================
582
+
583
+ if __name__ == "__main__":
584
+ demo = create_interface()
585
+ demo.launch(
586
+ server_name="0.0.0.0",
587
+ server_port=7861,
588
+ share=False,
589
+ )