suppressPackageStartupMessages({
library(dplyr)
library(huggingfaceR)
library(purrr)
library(reticulate)
library(stringr)
library(tibble)
library(tidyr)
})
source("R/use-nlg.R")
summary_model <- load_nlg_pipeline(
"qwen_1_5b_instruct",
"text-generation"
)
model_record <- summary_model$metadata |>
select(model_id, revision, license, task_scope)Writing a shorter account
Examine what a generated summary preserves and changes
Rosa prepares the morning brief for a neighborhood archive. She receives two service notes. Each is longer than the one sentence her colleagues need. A shorter sentence must still preserve the sourceβs meaning.
Abstractive summarization generates wording for a shorter account of a source. The output may reuse some source phrases, but it is not limited to selecting whole sentences. Lesson 54 used sentence selection. This lesson asks a local language model to write a new sentence. The lesson then compares the candidate with the original record.
This lesson shows how to:
- keep source records, prompts, and outputs linked to persistent IDs
- count the modelβs input tokens before generation
- compare generated summaries with a first-sentence baseline
- separate compression from source evidence and useful coverage
- examine names, numbers, and explicit limits without treating these checks as proof
- leave factual and editorial approval to a recorded human check.
Load the local model
Course setup downloads model files before rendering at the revision recorded in data/nlg-models.csv. The code below uses huggingfaceR to load that local version. R/use-nlg.R sets offline mode. If model files are missing, the lesson stops instead of starting a network download.
| Model | Revision | License | Course task |
|---|---|---|---|
| Qwen/Qwen2.5-1.5B-Instruct | 989aa7980e4cf806f80c7fef2b1adb7bc71aa306 | Apache-2.0 | abstractive summarization;paraphrasing;long-text generation;question answering;chatbot dialogue |
The model card documents an instruction-tuned causal language model. That description and license do not establish that its summaries are accurate.
Keep the source beside the brief
The author wrote these fictional teaching records for this lesson. They are not a random sample, a held-out benchmark, or evidence about archive services elsewhere.
Each brief states information that matters to the reader. SUM-01 needs the service, Tuesday schedule, and boundary on volunteer help. SUM-02 needs the event, photo-identification rule, and restricted-storage limit.
summary_sources <- tribble(
~source_id, ~brief, ~source_text,
"SUM-01",
paste(
"One sentence for staff; retain the job-search service, Tuesday help",
"schedule, and boundary on volunteer help."
),
paste(
"The Riverton library opens a new job-search room on Monday.",
"The room has 12 computers.",
"Volunteers offer free help on Tuesdays, but they do not complete",
"applications for visitors.",
"The pilot runs for six weeks."
),
"SUM-02",
paste(
"One sentence for visitors; retain the catalog workshop,",
"photo-identification rule, and restricted-storage limit."
),
paste(
"The neighborhood archive will open Room 5 on 14 September for a",
"catalog workshop led by Maya Chen.",
"Registration is free, but visitors must bring photo identification.",
"The session does not include access to restricted storage."
)
)| Source ID | Brief | Source text |
|---|---|---|
| SUM-01 | One sentence for staff; retain the job-search service, Tuesday help schedule, and boundary on volunteer help. | The Riverton library opens a new job-search room on Monday. The room has 12 computers. Volunteers offer free help on Tuesdays, but they do not complete applications for visitors. The pilot runs for six weeks. |
| SUM-02 | One sentence for visitors; retain the catalog workshop, photo-identification rule, and restricted-storage limit. | The neighborhood archive will open Room 5 on 14 September for a catalog workshop led by Maya Chen. Registration is free, but visitors must bring photo identification. The session does not include access to restricted storage. |
Declare required and optional facts
The author fixes the checklist before generation. A missing required item requires candidate revision. Optional clues remain useful during source checks, but their omission alone does not violate the brief.
summary_checks <- tribble(
~source_id, ~check_id, ~needed_information, ~required, ~pattern,
"SUM-01", "F1", "job-search service", TRUE,
"\\bjob[ -]search\\b",
"SUM-01", "F2", "Tuesday help", TRUE,
"\\bTuesday\\b",
"SUM-01", "F3", "volunteers do not complete applications", TRUE,
"\\b(not|no|never|without|do not|does not)\\b",
"SUM-01", "F4", "12 computers", FALSE,
"\\b12\\b",
"SUM-01", "F5", "six-week pilot", FALSE,
"\\b(six|6)[ -]week",
"SUM-02", "F6", "catalog workshop", TRUE,
"\\bworkshop\\b",
"SUM-02", "F7", "photo identification", TRUE,
"\\b(photo|identification|ID)\\b",
"SUM-02", "F8", "no restricted-storage access", TRUE,
"\\b(not|no|without|does not)\\b",
"SUM-02", "F9", "Room 5", FALSE,
"\\bRoom 5\\b",
"SUM-02", "F10", "14 September", FALSE,
"\\b14\\b.*\\bSeptember\\b",
"SUM-02", "F11", "Maya Chen", FALSE,
"\\bMaya Chen\\b",
"SUM-02", "F12", "free registration", FALSE,
"\\bfree\\b"
)| Source ID | Check | Predeclared information | Required by brief |
|---|---|---|---|
| SUM-01 | F1 | job-search service | TRUE |
| SUM-01 | F2 | Tuesday help | TRUE |
| SUM-01 | F3 | volunteers do not complete applications | TRUE |
| SUM-01 | F4 | 12 computers | FALSE |
| SUM-01 | F5 | six-week pilot | FALSE |
| SUM-02 | F6 | catalog workshop | TRUE |
| SUM-02 | F7 | photo identification | TRUE |
| SUM-02 | F8 | no restricted-storage access | TRUE |
| SUM-02 | F9 | Room 5 | FALSE |
| SUM-02 | F10 | 14 September | FALSE |
| SUM-02 | F11 | Maya Chen | FALSE |
| SUM-02 | F12 | free registration | FALSE |
Examine the input limit before generation
A model token is a tokenizer unit, not necessarily a word. The prompt includes the instruction, brief, and source text, so all three count against the input budget. This demonstration uses a 256-token limit and prohibits truncation.
summary_system <- paste(
"Write one sentence for the stated brief.",
"Use only information in the source.",
"Do not add a cause, recommendation, or new event."
)
summary_inputs <- summary_sources |>
mutate(
user_prompt = paste0(
"BRIEF: ", brief,
"\nSOURCE ID: ", source_id,
"\nSOURCE: ", source_text
),
model_prompt = map2_chr(
summary_system,
user_prompt,
\(system, user) {
nlg_chat_prompt(
summary_model$tokenizer,
system,
user
)
}
),
input_tokens = map_int(
model_prompt,
\(prompt) nlg_token_count(summary_model$tokenizer, prompt)
)
)| Source ID | Complete prompt tokens |
|---|---|
| SUM-01 | 120 |
| SUM-02 | 117 |
The workflow does not remove text to fit the limit. A longer document would need a stated policy before the model call. Options include separate sections or hierarchical summarization, which summarizes sections before combining their summaries.
Generate the candidate summaries
Greedy decoding chooses the highest-scoring next token at each step. The output limit is 72 new tokens. The call returns the generated text and token-level stopping information from the local model.
summary_results <- summary_inputs |>
mutate(
generation = map(
model_prompt,
\(prompt) {
nlg_generate(
summary_model,
prompt,
max_new_tokens = 72L,
repetition_penalty = 1.05
)
}
),
generated_summary = map_chr(generation, "text"),
output_tokens = map_int(generation, "output_tokens"),
ended_by_eos = map_lgl(generation, "ended_by_eos"),
hit_token_cap = map_lgl(generation, "hit_token_cap"),
first_sentence_baseline = map_chr(
str_split(source_text, "(?<=[.!?])\\s+"),
\(sentences) sentences[[1]]
)
)| Source ID | Generated abstractive candidate | First-sentence teaching baseline |
|---|---|---|
| SUM-01 | The Riverton library will continue to provide a job-search room with 12 computers, offer Tuesday volunteer help, and maintain a boundary on volunteer assistance during the pilot programβs six-week run. | The Riverton library opens a new job-search room on Monday. |
| SUM-02 | Visitors are encouraged to attend the catalog workshop, participate in photo-identification, and adhere to the restricted-storage limit at the neighborhood archiveβs opening on 14 September. | The neighborhood archive will open Room 5 on 14 September for a catalog workshop led by Maya Chen. |
The first-sentence baseline is available before model generation. The comparison does not match output lengths. It shows the difference between selecting source wording and generating a new account.
Measure shortening without calling it truth
The next table counts whitespace-separated words. That rule is easy to examine, but it is different from the model tokenizer used for the generation budget. A compression ratio is candidate length divided by source length. A ratio below 1 means the candidate is shorter. The ratio says nothing about whether the candidate kept the needed facts.
count_words <- function(text) {
str_count(str_squish(text), "\\S+")
}
compression <- summary_results |>
transmute(
source_id,
source_words = map_int(source_text, count_words),
generated_words = map_int(generated_summary, count_words),
first_sentence_words = map_int(
first_sentence_baseline,
count_words
),
generated_to_source = generated_words / source_words
)| Source ID | Source words | Generated words | Baseline words | Generated/source ratio |
|---|---|---|---|---|
| SUM-01 | 35 | 30 | 10 | 0.86 |
| SUM-02 | 36 | 25 | 18 | 0.69 |
The model can produce a short sentence by selecting relevant content, omitting necessary content, or both. Length cannot distinguish those cases.
Examine facts chosen before generation
Rosa writes a checklist before reading the output. A regular expression is a pattern for matching text. The expressions below find visible strings related to that checklist. A missing match suggests a possible omission to examine. A match does not establish the correct role or limits of a name, number, or negative statement.
summary_flags <- summary_checks |>
left_join(
summary_results |>
select(source_id, generated_summary),
by = "source_id"
) |>
mutate(
text_flag = str_detect(
generated_summary,
regex(pattern, ignore_case = TRUE)
)
) |>
select(
source_id,
check_id,
needed_information,
required,
text_flag
)
summary_actions <- summary_flags |>
group_by(source_id) |>
summarise(
decision = list(nlg_required_fact_action(required, text_flag)),
optional_clues_missing = sum(!required & !text_flag),
.groups = "drop"
) |>
mutate(
missing_required = map_int(decision, "missing_required"),
action = if_else(
map_lgl(decision, "revision_needed"),
"revise",
"send to human source review"
)
) |>
select(
source_id,
missing_required,
optional_clues_missing,
action
)| Source ID | Check | Information chosen before generation | Required by brief | Surface flag present |
|---|---|---|---|---|
| SUM-01 | F1 | job-search service | TRUE | TRUE |
| SUM-01 | F2 | Tuesday help | TRUE | TRUE |
| SUM-01 | F3 | volunteers do not complete applications | TRUE | FALSE |
| SUM-01 | F4 | 12 computers | FALSE | TRUE |
| SUM-01 | F5 | six-week pilot | FALSE | TRUE |
| SUM-02 | F6 | catalog workshop | TRUE | TRUE |
| SUM-02 | F7 | photo identification | TRUE | TRUE |
| SUM-02 | F8 | no restricted-storage access | TRUE | FALSE |
| SUM-02 | F9 | Room 5 | FALSE | FALSE |
| SUM-02 | F10 | 14 September | FALSE | TRUE |
| SUM-02 | F11 | Maya Chen | FALSE | FALSE |
| SUM-02 | F12 | free registration | FALSE | FALSE |
| Source ID | Missing required facts | Missing optional clues | Next action |
|---|---|---|---|
| SUM-01 | 1 | 0 | revise |
| SUM-02 | 1 | 3 | revise |
This check is intentionally incomplete. It cannot detect a changed relation, an incorrect number association, a cause absent from the source, or a fluent generalization. Those errors require comparison of the source and candidate.
Compare each summary with the volunteer and storage rules in its source. For SUM-01, volunteers offer Tuesday help but do not complete applications for visitors. For SUM-02, the session does not include access to restricted storage. A general statement about limits does not preserve either rule. The string checks can miss changes in these relationships.
N-gram overlap metrics such as ROUGE compare word sequences with a reference under a specified normalization rule. Normalization standardizes text before comparison. These metrics do not measure the share of claims with evidence in the source.
Record the limit of automated checks
Execution can establish that a model executed, stayed within the configured limits, and returned text. It cannot grant factual or editorial approval.
summary_review <- tibble(
review = c(
"local model execution",
"prompt input within 256 tokens",
"output stopped at EOS or token cap",
"required-fact surface screen",
"human checks of source evidence",
"human checks of usefulness and prose"
),
status = c(
sprintf(
"completed: %d candidates",
sum(nzchar(summary_results$generated_summary))
),
sprintf(
"within budget: %d/%d candidates",
sum(summary_inputs$input_tokens <= 256L),
nrow(summary_inputs)
),
sprintf(
"recorded: %d/%d candidates",
sum(summary_results$ended_by_eos | summary_results$hit_token_cap),
nrow(summary_results)
),
sprintf(
"held: %d/%d candidates missing required facts",
sum(summary_actions$missing_required > 0L),
nrow(summary_actions)
),
"pending",
"pending"
)
)| Check stage | Status |
|---|---|
| local model execution | completed: 2 candidates |
| prompt input within 256 tokens | within budget: 2/2 candidates |
| output stopped at EOS or token cap | recorded: 2/2 candidates |
| required-fact surface screen | held: 2/2 candidates missing required facts |
| human checks of source evidence | pending |
| human checks of usefulness and prose | pending |
Before publication, a reader must compare each summary claim with its source. The reader must find omissions of essential information. The reader must reject additions without source evidence. A fluent one-sentence result is only a candidate.
What to remember
- Abstractive summarization generates wording. Extractive summarization selects source text.
- Count the complete prompt with the model tokenizer.
- Disclose any truncation.
- Compression describes length, not coverage or source evidence.
- Examine omissions and additions against a brief written before generation.
- Names, numbers, and negation flags are prompts for human checks, not semantic proof.
- Keep model execution separate from human factual and editorial approval.