When you build a presentation, the part that really costs time is rarely the layout — it is deciding what to say. A slide only has room for the essentials: a few short lines, a set of numbers, one chart. The words that actually need to be spoken live in the author's head. The night before the talk, the author reads the whole deck again, slide by slide, trying to remember what each page was meant to say, and then writes it up.

It gets worse when a team is involved. For a product launch, one person writes the content, another builds the slides, and a third delivers the talk. Whoever steps on stage inherits a deck of bullet points with no idea which numbers matter most — so they improvise. Speaker notes exist precisely for this: they never appear on the projector, only in Presenter View, as the author's message to the speaker. In practice, almost nobody has the patience to fill them in page by page.

The PowerPoint AI capability in Spire.Agent.Office lets you describe in plain language what the notes should say and what tone the script should take. The AI agent reads the title and bullet points of every slide and then handles two jobs: writing speaker notes for each slide, and expanding the whole deck into a Word script that can be read aloud directly.

Compared with the Traditional SDK API

Traditional Spire.Office for .NET API Spire.Agent.Office
Driving model Loop over slides, pull text out of each shape, assemble the copy yourself, then write it back to the notes page — every step is code Describe the writing requirements in natural language and let the AI plan the execution path
Code size 150-300 lines of C# (slide iteration, shape traversal, text extraction, notes-page writing, font and paragraph settings) About 10 lines of calling code plus one natural-language instruction
Content generation You must wire up a model yourself or hand-write templates, and it is hard to keep each slide in context The AI understands each slide's points and writes copy that reads coherently page to page
Layout handling Writing notes easily damages the original layout and placeholder structure The AI preserves the original layout, fonts and colours, appending only to the notes pane
Changing requirements Adjust tone, length or script format → edit code → rebuild → redeploy Edit the wording of the instruction and it takes effect immediately

This article shows how to use Spire.Agent.Office to generate speaker notes and a script for a PowerPoint deck. Two cases cover the two common patterns — adding notes to an existing deck and turning a report into a Word script:

For product installation and SpireToken configuration, please refer to Integrating Spire.Agent.Office in a .NET Project. The examples below assume Spire.Agent.Office is installed and SpireToken is configured.


Case 1: Generate per-slide speaker notes for a deck

The most common situation is a deck that is already final: layout and content stay exactly as they are, and the only thing missing is the prompt for whoever has to speak. A projected slide is built to be read at a glance, so it carries a title and a few bullet points — where the emphasis falls, what is behind each number, and how to move from one page to the next all stay in the author's head. Speaker notes are what carry that across. They are the author's handover to the presenter: what this page is really getting at, what to stress, and how to lead naturally into the next one. This product launch deck has 8 slides, each with a title and a few bullet points, and an empty notes pane from start to finish. The presenter needs one short paragraph per slide — not an essay, but enough to present the deck rather than read it out. Writing those notes by hand is expensive because every slide needs its own thought. Read the page, rebuild the sentence, and calibrate length — too short and it is no help, too long and it turns into reading from a script. Eight slides is tolerable; a fifty-page annual review rarely gets the treatment it deserves.

The example below uses the Spire.Agent.Office agent to read every slide and write speaker notes into the notes pane, ready to be read aloud:

using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Presentation;

// PowerPoint processing configuration
string inputPath = @"E:\product-launch.pptx";  // Deck to add speaker notes to
string savePath = @"E:\product_result.pptx";  // Result document path

string key = "**************************";  // SpireToken key
string instruction = "Generate speaker notes for each slide of the input PPT file and write them into the notes pane of that slide: " +
"1. Read each slide's title and bullet points and understand what the page is meant to convey; " +
"2. Write one paragraph of speaker notes per slide, in the presenter's voice, ready to be read aloud; " +
"3. Each note should make clear what this page emphasises and lead naturally into the next slide; keep the notes varied in depth — explain the important content in detail and keep transition slides brief; " +
"4. Keep the original slide layout, fonts, colours and page order completely unchanged, and do not add or remove any slides; " +
"Finally, save the output as a PowerPoint file";

// Call the PowerPoint processing function
AIResult result = ExecuteDemoPpt(instruction, inputPath, savePath, key);

// Run the PowerPoint AI processing
static AIResult ExecuteDemoPpt(string instruction, string inputPath, string savePath, string key)
{
    // Create the AIOptions configuration object
    AIOptions options = new AIOptions();
    options.SpireToken = key;  // Set the SpireToken key

    // Process the PowerPoint document with a Presentation object
    using (Presentation ppt = new Presentation())
    {
        // Load the PowerPoint document from file
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            ppt.LoadFromFile(inputPath);
        }
        // Create the AI document processor
        AIDocumentProcessor processor = ppt.AI(options);

        // Execute the AI instruction
        return processor.ExecuteInstruction(ppt, instruction, savePath);
    }
}

The original deck with an empty notes pane The original deck with an empty notes pane The deck after speaker notes were written slide by slide The deck after speaker notes were written slide by slide

The generated deck looks identical to the original on the projector; the notes appear only in Presenter View. The speaker opens Presenter View and sees the prompt for each slide below it, ready to deliver a complete product launch in order.


Case 2: Turn a business review into a ready-to-read Word script

The notes pane is good for prompting, but not for reading aloud. Notes are short fragments in a small font at the edge of the screen; looking down at them on stage hurts delivery and invites missed lines. When the occasion is more formal — reporting quarterly results to management, for instance, where every figure has to be explained — the speaker needs a complete, spoken-word script. The medium for that script deserves a word of its own. A presentation is made to be looked at; a script is made to be read. A script needs control over font size, line spacing and sectioning, and it is usually printed and carried into the room. Packing several hundred words of body text onto each slide reads badly — and projecting it hands the audience every line. So this case takes a PPT as input and produces a Word script document as output.

Handling that format change is exactly what Spire.Agent.Office does: the deck is still loaded with Presentation, the AI reads each slide, and it writes the script as a Word file following the instruction. So this case has the AI read the figures page by page and then write the script as sections in the original order: each slide of the original becomes one section of the script document, keeping the original title, with the body holding the full script text.

The example below uses the Spire.Agent.Office agent to interpret the figures page by page and write the script, producing a Word document:

using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Presentation;

// PowerPoint processing configuration
string inputPath = @"E:\quarterly-review.pptx";  // Input presentation path
string savePath = null;  // Result document path (null here, the output folder below is used instead)
string OutDir = @"E:\output-script";  // Output directory
string key = "**************************";  // SpireToken key
string instruction =
    "Read the input PPT file and write a complete presentation script for the speaker, outputting it as a Word script document: " +
    "1. Read the figures and bullet points on each slide and understand the core conclusion of that page; " +
    "2. Write a complete, conversational script per slide that can be read aloud directly; keep it varied in depth, quote the specific figures on that page and explain what they mean for the business; " +
    "3. Lay the script out as sections in the original slide order, each headed 'Slide N + the original title', with the full script for that page as the body; " +
    "4. Format the body for reading aloud: a clear typeface, no smaller than 14 pt, 1.5 line spacing, comfortable paragraph spacing;";

// Call the PowerPoint processing function
AIResult result = ExecuteDemoPpt(instruction, inputPath, savePath, key, OutDir);

// Run the PowerPoint AI processing
static AIResult ExecuteDemoPpt(string instruction, string inputPath, string savePath, string key, string output)
{
    // Create the AIOptions configuration object
    AIOptions options = new AIOptions();
    options.WorkDir = output;  // Set the working directory to the output directory
    options.SpireToken = key;  // Set the SpireToken key

    // Process the PowerPoint document with a Presentation object
    using (Presentation ppt = new Presentation())
    {
        // Load the PowerPoint document from file
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            ppt.LoadFromFile(inputPath);
        }
        // Create the AI document processor
        AIDocumentProcessor processor = ppt.AI(options);

        // Execute the AI instruction
        return processor.ExecuteInstruction(ppt, instruction, savePath);
    }
}

The generated Word script document The generated Word script document


FAQ

Can I use my own model or a private deployment?

Note: By default the AI requests go to Spire's model service. AIOptions also exposes Model, BaseUrl and ApiKey for the model name, the service address and the access key — set these three to route the requests to your own or a third-party inference service.

AIOptions options = new AIOptions();
options.BaseUrl = "baseUrl";
options.Model = "modelName";
options.SpireToken = "*************";
options.ApiKey = "*************";

A long deck is slow to process — and may time out

Cause: Notes and scripts are generated page by page, so the AI reads and writes the document repeatedly and sends the existing content back to the model each time. The more slides there are, and the denser the text on each, the faster token consumption grows and the longer the whole run takes — the default timeout is often not enough.

Solution: Set TimeoutMs explicitly on the AIOptions instance (the unit is milliseconds) so a long deck has enough time. For a 50-slide PPT file, TimeoutMs can be set to 10 minutes (600000).


Getting a SpireToken Key

Configure it in code:

AIOptions options = new AIOptions();
options.SpireToken = key;

Inserting a page break in a Word document is the most common way to control where a chapter begins and to keep a heading from being split from its body text across a page boundary; cleaning up the redundant page breaks in a document, on the other hand, is the key step in dealing with the blank pages left behind after content is copied from another document. Spire.Doc for JavaScript carries out these operations directly in the browser via WebAssembly, using a virtual file system (VFS) to manage input and output files — no backend server required.

This article covers two core features:

For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.


Insert a Page Break

Inserting a page break involves three steps: first, load the font files and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the document, locate the target paragraph and call AppendBreak with BreakType.PageBreak, which appends the page break to the end of that paragraph as one of its child objects; finally, read the saved file from VFS, wrap it as a Blob and trigger a browser download.

function App() {
  const InsertPageBreak = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the target Word document into VFS
    const inputFileName = "Template_Docx_1.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // Create a Document instance and load the document
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // Locate the fourth paragraph of the first section and append a page break to its end
    doc.Sections.get_Item(0).Paragraphs.get_Item(3).AppendBreak(docModule.BreakType.PageBreak);

    // Define the output file name
    const outputFileName = "InsertPageBreak_out.docx";

    // Save the document to VFS
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Insert a Page Break into a Word Document</h1>
      <button onClick={InsertPageBreak}>Generate</button>
    </div>
  );
}
export default App;

The document after a page break is appended to the end of the fourth paragraph

The document after a page break is appended to the end of the fourth paragraph


Remove Page Breaks

Removing page breaks also involves three steps: first, load the font files and the Word document that contains the page breaks into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the document, walk through the child objects of every paragraph in each section, test with DocumentObjectType whether an object is a page break, and remove it with ChildObjects.Remove; finally, read the saved file from VFS, wrap it as a Blob and trigger a browser download.

function App() {
  const RemovePageBreaks = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the target Word document into VFS
    const inputFileName = "Template_Docx_4.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // Create a Document instance and load the document
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // Get the first section
    const section = doc.Sections.get_Item(0);

    // Walk through every paragraph in the section
    for (let j = 0; j < section.Paragraphs.Count; j++) {
        const p = section.Paragraphs.get_Item(j);

        // Walk the paragraph's child objects backwards, so that removing an element does not shift the indices still to come
        for (let i = p.ChildObjects.Count - 1; i >= 0; i--) {
            const obj = p.ChildObjects.get_Item(i);

            // Test whether the object is a page break
            if (obj.DocumentObjectType == docModule.DocumentObjectType.Break
                && obj.BreakType == docModule.BreakType.PageBreak) {
                // Remove the page break from the paragraph
                p.ChildObjects.Remove(obj);
            }
        }
    }

    // Define the output file name
    const outputFileName = "RemovePageBreaks_out.docx";

    // Save the document to VFS
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Remove Page Breaks from a Word Document</h1>
      <button onClick={RemovePageBreaks}>Generate</button>
    </div>
  );
}
export default App;

The document after every page break has been removed

The document after every page break has been removed


FAQ

An extra blank line appears at the top of the new page after inserting a page break

Cause: AppendBreak appends the page break to the end of the target paragraph, so that paragraph's own spacing after it is carried over to the start of the new page along with the page break, which shows up as a strip of white space at the top of the new page. If the paragraph has automatic spacing enabled, the height of that strip also changes with the font size.

Solution: Clear the spacing after the target paragraph before inserting the page break:

const para = document.Sections.get_Item(0).Paragraphs.get_Item(3);

// Turn off automatic spacing after the paragraph and set the space after it to 0
para.Format.AfterAutoSpacing = false;
para.Format.AfterSpacing = 0;

// Then insert the page break
para.AppendBreak(wasmModule.BreakType.PageBreak);

The document is still paginated after the page breaks are removed

Cause: Not every page break in a document comes from a page break object. If a paragraph itself has the page-break-before property set, that paragraph will still start on a new page even after the page break objects have been removed; walking ChildObjects and deleting DocumentObjectType.Break objects alone cannot take that kind of pagination out.

Solution: Reset the paragraph's page-break-before property at the same time as the page break objects are removed:

const section = document.Sections.get_Item(0);

for (let j = 0; j < section.Paragraphs.Count; j++) {
  const p = section.Paragraphs.get_Item(j);

  // Clear the page-break-before property set on the paragraph
  if (p.Format.PageBreakBefore) {
    p.Format.PageBreakBefore = false;
  }
}

Get a Free License

Spire.Doc for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.

A gutter is the extra white space a page sets aside for binding, so that the body text is not hidden by the staples or the glued spine once the document is printed and bound. Because it is part of the page setup, it has to be fixed when the document is generated and cannot be added later at the printing stage. Spire.Doc for JavaScript performs page setup directly in the browser via WebAssembly, using a virtual file system (VFS) to manage input and output files — no backend server required.

This article covers two core features:

For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.


Add a Gutter

Adding a gutter involves three steps: first, load the font files and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the document, get the target section and set the gutter width through PageSetup.Gutter — the value is in points and applies to the left side of the page by default; finally, read the saved file from VFS, wrap it as a Blob and trigger a browser download.

function App() {
  const AddGutter = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the target Word document into VFS
    const inputFileName = "GutterSample.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // Create a Document instance and load the document
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // Get the first section
    const section = doc.Sections.get_Item(0);

    // Set the gutter width in points; it applies to the left side of the page by default
    section.PageSetup.Gutter = 100;

    // Define the output file name
    const outputFileName = "AddGutter_out.docx";

    // Save the document to VFS
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // Release resources
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Add a Gutter to a Word Document</h1>
      <button onClick={AddGutter}>Generate</button>
    </div>
  );
}
export default App;

The document after a gutter has been added

The document after a gutter has been added


Set the Gutter Position

The gutter position decides which side of the page the strip of white space is added to: PageSetup.IsTopGutter set to true means a top gutter, and false (the default) means a left gutter. The steps are the same three as before: first, load the font files and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the document, get the target section and set PageSetup.IsTopGutter to true to turn on the top gutter, then set the gutter width through PageSetup.Gutter; finally, read the saved file from VFS, wrap it as a Blob and trigger a browser download.

function App() {
  const SetGutterPosition = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the target Word document into VFS
    const inputFileName = "GutterSample.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // Create a Document instance and load the document
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // Get the first section
    const section = doc.Sections.get_Item(0);

    // Set the gutter position to the top
    section.PageSetup.IsTopGutter = true;

    // Set the gutter width in points
    section.PageSetup.Gutter = 100;

    // Define the output file name
    const outputFileName = "SetGutterPosition_out.docx";

    // Save the document to VFS
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // Release resources
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Set the Gutter Position in a Word Document</h1>
      <button onClick={SetGutterPosition}>Generate</button>
    </div>
  );
}
export default App;

The document after the top gutter has been set

The document after the top gutter has been set


FAQ

The gutter width does not match the value shown in the Word interface

Cause: PageSetup.Gutter is measured in points, while the Word Page Setup dialog box displays centimeters or inches by default. Passing 100 directly, as the examples do, means 100 points — about 3.53 centimeters — which is noticeably wide on an A4 page.

Solution: Convert the unit before passing the value in. One point is 1/72 inch and one inch is 2.54 centimeters, so the formula for converting centimeters to points is points = centimeters ÷ 2.54 × 72:

// Convert a 1.5-centimeter gutter width to points
const gutterInPoints = (1.5 / 2.54) * 72;
section.PageSetup.Gutter = gutterInPoints;

The text area becomes too narrow after a gutter is set

Cause: The gutter claims space in addition to the margins; it does not enlarge the page but squeezes the text area inwards. If the existing margins were already generous, adding a gutter narrows the usable width further, leaving too few characters per line and making paragraphs wrap far more often.

Solution: Reduce the margin on the same side at the same time as the gutter is set, so that the usable text width is preserved:

const pageSetup = section.PageSetup;

// Set a 1.5-centimeter gutter (centimeters converted to points)
pageSetup.Gutter = (1.5 / 2.54) * 72;

// The gutter takes up space on the left, so reduce the left margin accordingly
pageSetup.Margins.Left = 60;

Get a Free License

Spire.Doc for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.

page 18