Strip markdown decoration from model-emitted insight titles
Models wrap the title line despite the prompt — "**Title: A Day in the Woods**", "## Title: ...", bold around just the label — which made parse_title_body's bare "Title:" prefix match fall through to the fallbacks and leak asterisks into the stored title. strip_title_markdown trims bold/italic markers, heading hashes, backticks, and quotes from both ends; applied to the label line, the extracted title, both fallback paths, and generate_photo_title (which previously stripped only quotes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+44
-12
@@ -33,30 +33,40 @@ use crate::utils::{earliest_fs_time, normalize_path};
|
|||||||
/// and labels the truncation via `found_header`.
|
/// and labels the truncation via `found_header`.
|
||||||
const LOCATION_HISTORY_DISPLAY_LIMIT: usize = 20;
|
const LOCATION_HISTORY_DISPLAY_LIMIT: usize = 20;
|
||||||
|
|
||||||
|
/// Strip common markdown decoration (bold/italic markers, heading hashes,
|
||||||
|
/// backticks, quotes) from both ends of a model-emitted title. Models wrap
|
||||||
|
/// the line despite the prompt: `**Title: A Day in the Woods**`,
|
||||||
|
/// `## Title: ...`, `"..."`.
|
||||||
|
pub(crate) fn strip_title_markdown(s: &str) -> &str {
|
||||||
|
s.trim_matches(|c: char| matches!(c, '*' | '_' | '`' | '#' | '"') || c.is_whitespace())
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a "Title: ...\n\n<body>" response into (title, body).
|
/// Parse a "Title: ...\n\n<body>" response into (title, body).
|
||||||
/// Falls back to the first sentence as the title if the model didn't
|
/// Falls back to the first sentence as the title if the model didn't
|
||||||
/// follow the format.
|
/// follow the format.
|
||||||
pub(crate) fn parse_title_body(raw: &str) -> (String, String) {
|
pub(crate) fn parse_title_body(raw: &str) -> (String, String) {
|
||||||
let trimmed = raw.trim();
|
let trimmed = raw.trim();
|
||||||
|
|
||||||
// Try "Title: <title>\n\n<body>" or "Title: <title>\n<body>"
|
// Try "Title: <title>\n<body>", tolerating markdown decoration around
|
||||||
if let Some(rest) = trimmed
|
// the title line.
|
||||||
|
let (first_line, rest) = match trimmed.find('\n') {
|
||||||
|
Some(pos) => (&trimmed[..pos], trimmed[pos..].trim()),
|
||||||
|
None => (trimmed, ""),
|
||||||
|
};
|
||||||
|
let first_line = strip_title_markdown(first_line);
|
||||||
|
if let Some(t) = first_line
|
||||||
.strip_prefix("Title:")
|
.strip_prefix("Title:")
|
||||||
.or_else(|| trimmed.strip_prefix("title:"))
|
.or_else(|| first_line.strip_prefix("title:"))
|
||||||
{
|
{
|
||||||
let rest = rest.trim_start();
|
let title = strip_title_markdown(t);
|
||||||
if let Some(split_pos) = rest.find("\n\n").or_else(|| rest.find('\n')) {
|
if !title.is_empty() && !rest.is_empty() {
|
||||||
let title = rest[..split_pos].trim();
|
return (title.to_string(), rest.to_string());
|
||||||
let body = rest[split_pos..].trim();
|
|
||||||
if !title.is_empty() && !body.is_empty() {
|
|
||||||
return (title.to_string(), body.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: first sentence (up to first `. ` or `.\n`) becomes the title
|
// Fallback: first sentence (up to first `. ` or `.\n`) becomes the title
|
||||||
if let Some(pos) = trimmed.find(". ").or_else(|| trimmed.find(".\n")) {
|
if let Some(pos) = trimmed.find(". ").or_else(|| trimmed.find(".\n")) {
|
||||||
let title = &trimmed[..pos];
|
let title = strip_title_markdown(&trimmed[..pos]);
|
||||||
let body = trimmed[pos + 1..].trim();
|
let body = trimmed[pos + 1..].trim();
|
||||||
if title.len() <= 100 && !body.is_empty() {
|
if title.len() <= 100 && !body.is_empty() {
|
||||||
return (title.to_string(), body.to_string());
|
return (title.to_string(), body.to_string());
|
||||||
@@ -65,7 +75,7 @@ pub(crate) fn parse_title_body(raw: &str) -> (String, String) {
|
|||||||
|
|
||||||
// Last resort: truncate to 60 chars for title, full text as body
|
// Last resort: truncate to 60 chars for title, full text as body
|
||||||
let title: String = trimmed.chars().take(60).collect();
|
let title: String = trimmed.chars().take(60).collect();
|
||||||
let title = title.trim_end().to_string();
|
let title = strip_title_markdown(title.trim_end()).to_string();
|
||||||
(title, trimmed.to_string())
|
(title, trimmed.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5126,6 +5136,28 @@ mod tests {
|
|||||||
assert_eq!(b, "Everyone gathered...");
|
assert_eq!(b, "Everyone gathered...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_title_body_strips_bold_wrapper() {
|
||||||
|
let (t, b) = parse_title_body("**Title: A Day in the Woods**\n\nWe hiked the ridge trail.");
|
||||||
|
assert_eq!(t, "A Day in the Woods");
|
||||||
|
assert_eq!(b, "We hiked the ridge trail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_title_body_strips_bold_label_only() {
|
||||||
|
// Bold around just the label: "**Title:** X"
|
||||||
|
let (t, b) = parse_title_body("**Title:** Garden Party\n\nEveryone gathered...");
|
||||||
|
assert_eq!(t, "Garden Party");
|
||||||
|
assert_eq!(b, "Everyone gathered...");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_title_body_strips_heading_hashes() {
|
||||||
|
let (t, b) = parse_title_body("## Title: Morning Walk\nThe sun was rising...");
|
||||||
|
assert_eq!(t, "Morning Walk");
|
||||||
|
assert_eq!(b, "The sun was rising...");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_title_body_fallback_first_sentence() {
|
fn parse_title_body_fallback_first_sentence() {
|
||||||
let (t, b) = parse_title_body("A warm summer day. We gathered at the park for a picnic.");
|
let (t, b) = parse_title_body("A warm summer day. We gathered at the park for a picnic.");
|
||||||
|
|||||||
+10
-1
@@ -548,7 +548,16 @@ Capture the key moment or theme. Return ONLY the title, nothing else."#,
|
|||||||
let title = self
|
let title = self
|
||||||
.generate_with_images(&prompt, Some(system), None)
|
.generate_with_images(&prompt, Some(system), None)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(title.trim().trim_matches('"').to_string())
|
// Models decorate despite "Return ONLY the title": quotes, bold
|
||||||
|
// markers, sometimes a "Title:" label.
|
||||||
|
use crate::ai::insight_generator::strip_title_markdown;
|
||||||
|
let cleaned = strip_title_markdown(title.trim());
|
||||||
|
let cleaned = cleaned
|
||||||
|
.strip_prefix("Title:")
|
||||||
|
.or_else(|| cleaned.strip_prefix("title:"))
|
||||||
|
.map(strip_title_markdown)
|
||||||
|
.unwrap_or(cleaned);
|
||||||
|
Ok(cleaned.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a summary for a single photo based on its context
|
/// Generate a summary for a single photo based on its context
|
||||||
|
|||||||
Reference in New Issue
Block a user