Files
FamilyMealPlanner/MealMood/Services/IngredientParser.swift
T
alexandrev-tibco 8e4d0c2d89 2.0: dictado de platos/ingredientes + calendario más alto en iPad/Mac
- Dictado por voz (Speech/SFSpeechRecognizer, on-device) en el nombre de
  plato y en ingredientes, con transcripción en vivo.
- Un solo texto dictado se separa en ingredientes individuales vía Apple
  Foundation Models on-device (IngredientParser), con fallback heurístico.
- Permisos de micrófono y reconocimiento de voz en Info.plist.
- Cadenas de dictado en los 6 idiomas.
- iPad/Mac: el calendario reclama ~50% de la altura disponible en lugar
  de quedar fijo a ~240px; iPhone (compact) sin cambios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3HaWmmtTQ1vdTERtSYU6p
2026-07-22 18:16:51 +02:00

62 lines
2.8 KiB
Swift

import Foundation
#if canImport(FoundationModels)
import FoundationModels
#endif
/// Splits a single free-form phrase (typically dictated, e.g. "two onions, half
/// a litre of milk and some olive oil") into individual, normalized ingredient
/// lines. Uses Apple's on-device Foundation Models when available for smart
/// parsing of quantities and natural-language separators; otherwise falls back
/// to a lightweight heuristic splitter. Fully local and never throws.
enum IngredientParser {
/// Parses `text` into clean ingredient lines, kept in `language`.
static func parse(_ text: String, language: AppLanguage) async -> [String] {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return [] }
#if canImport(FoundationModels)
if #available(iOS 26.0, *), case .available = SystemLanguageModel.default.availability {
let langName = language.resolved().displayName
let instructions = """
You split a spoken grocery list into individual ingredients. The user \
dictates several ingredients in one sentence, separated by commas, the \
word "and", or natural pauses. Output ONE ingredient per line, keeping \
any quantity the user actually said (e.g. "2 onions", "500 g flour"). \
Do not invent ingredients, do not add quantities that were not said, \
do not number or bullet the lines, and add no commentary. Keep the \
ingredients written in \(langName).
"""
do {
let session = LanguageModelSession(instructions: instructions)
let response = try await session.respond(to: trimmed)
let lines = IngredientGenerator.parseLines(response.content)
if !lines.isEmpty { return lines }
} catch {
CrashlyticsService.record(error, context: "ingredient_parsing")
}
}
#endif
return heuristicSplit(trimmed)
}
/// Splits on newlines, commas, semicolons and standalone conjunctions
/// ("and"/"y"/"e"/"et"/"und") when the on-device model is unavailable.
static func heuristicSplit(_ text: String) -> [String] {
let separators = CharacterSet(charactersIn: ",;\n")
let conjunctions = [" y ", " e ", " and ", " et ", " und "]
return text
.components(separatedBy: separators)
.flatMap { piece -> [String] in
var parts = [piece]
for conjunction in conjunctions {
parts = parts.flatMap { $0.components(separatedBy: conjunction) }
}
return parts
}
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
}
}