
Foundation Models로 긴 녹취록을 요약할 때 적용한 분할·재요약·실패 처리
요약
FoundationModels를 활용하여 긴 동영상 녹취록을 요약하는 macOS 앱 구현 사례를 다룹니다. 긴 텍스트의 분할 및 재요약 전략, 가드레일 거부와 일반 실패를 구분하는 예외 처리, 짧은 텍스트에 대한 요약 방지 로직 등 실무적인 구현 노하우를 공유합니다.
핵심 포인트
- 긴 녹취록은 글자 수 기준으로 분할 후 재요약하는 전략 사용
- 가드레일 거부(blocked)와 일반 실패를 구분하여 UI 대응
- Apple Intelligence 가용 여부에 따른 기능 활성화 제어
- 콘텐츠 변환 목적에 맞는 가드레일 설정으로 거부율 감소
- 의미 있는 요약을 위해 최소 글자 수 기준(140자) 적용
macOS 앱에서 동영상 녹취록 결과를 FoundationModels로 요약하는 기능을 구현했습니다.
이 기사에서는 FoundationModels의 기본적인 사용법이 아니라, 실제 앱에 적용했을 때 필요해진 다음과 같은 처리에 집중합니다.
- Apple Intelligence를 사용할 수 없는 환경에서는 요약만 비활성화
- 너무 짧은 녹취록은 요약하지 않음
- 긴 녹취록은 분할한 뒤 재요약함
- 가드레일(Guardrail) 거부와 일반 실패를 구분
- 생성 결과의 반복을 감지하여 폐기
녹취록 결과는 이미 다음과 같은 Segment 배열 형태로 가지고 있습니다.
struct Segment: Codable, Sendable, Equatable, Identifiable {
var id = UUID()
var time: TimeInterval
...
요약에 전달할 때는 Segment의 본문만 연결합니다.
let transcript = segments.map(\.text).joined(separator: " ")
FoundationModels의 요약은 Apple Intelligence를 사용할 수 있는 환경에서만 작동합니다.
앱 전체를 비활성화하는 것이 아니라, 요약 기능만 옵션으로 취급하고 싶었기에 다음과 같은 판정을 준비했습니다.
import FoundationModels
enum SummaryService {
static var summarizationAvailable: Bool {
...
녹취나 번역은 사용할 수 있지만, 요약만 사용할 수 없는 환경이 있습니다. UI 측에서는 이 값을 보고 요약 버튼이나 설명 표시를 전환하고 있습니다.
동영상 녹취록을 요약하는 처리는 사용자 자신의 콘텐츠를 변환하는 처리입니다.
따라서 모델은 다음과 같이 만들었습니다.
private static let summaryModel = SystemLanguageModel(
guardrails: .permissiveContentTransformations
)
이것은 '안전 판정을 해제한다'는 의미가 아니라, Foundation Models가 제공하는 콘텐츠 변환(content transformation)용 가드레일(guardrails)을 선택하는 구현입니다.
기본 가드레일(default guardrails)을 사용하면 일반적인 인터뷰나 대화 요약에서도 거부되는 경우가 있었습니다. 녹취록 변환 용도에서는 용도에 맞는 가드레일로 맞추는 것이 실용적이었습니다.
요약 결과를 String?로만 처리하면,
- 모델을 사용할 수 없음
- 녹취록이 너무 짧음
- 생성에 실패함
- 안전 가드레일(safety guardrail)로 인해 거부됨
위 사항들을 구분할 수 없습니다.
UI에서 보여주고 싶은 표시가 다르기 때문에 결과 타입을 나누었습니다.
enum SummaryOutcome: Sendable {
case text(String)
case blocked
...
blocked는 모델의 안전 가드(safety guard)에 의해 거부된 경우이며, none은 요약이 불필요하거나 일반적인 실패로 취급합니다.
짧은 녹취록을 요약하면 원래 문장을 단순히 바꾸어 말하는 수준에 그치기 쉽습니다.
그래서 일정 글자 수 미만은 요약하지 않도록 했습니다.
static let minSummaryChars = 140
static func summarize(
_ transcript: String,
...
UI 측에서는 "요약할 만큼의 길이가 아니므로 녹취록을 확인해 주세요"라고 처리하고 있습니다.
긴 동영상의 녹취록을 그대로 한 번의 respond에 던지면, 컨텍스트 상한(context limit)이나 생성 실패에 부딪힙니다.
구현에서는 글자 수 기준으로 분할하여 각 청크(chunk)를 요약한 뒤, 청크 요약본을 다시 요약하는 방식을 취하고 있습니다.
static func summarize(
_ transcript: String,
outputLanguage: String,
...
분할된 청크 요약 중 어느 곳에서라도 blocked가 발생하면 전체를 blocked로 처리합니다. 어떤 청크가 안전한지 앱 측에서 판정하기 시작하면 복잡해지기 때문입니다.
마지막 return .text(combined)는 청크 요약을 합친 문자열조차 너무 길 경우를 대비한 폴백(fallback)입니다. 이상적인 하나의 요약본은 아니지만, 아무것도 나오지 않는 것보다는 사용자에게 유용했습니다.
구현 시에는 단순히 글자 수로 자르지 않고, 공백으로 구분된 단어 경계(word boundary)를 기준으로 분할합니다.
private static func chunked(_ text: String, max: Int) -> [String] {
var chunks: [String] = []
var current = ""
...
일본어만 있는 녹취록은 공백이 적기 때문에 엄격한 일본어 분할 방식은 아닙니다. 그럼에도 불구하고 영어 또는 혼합 텍스트를 포함하는 실제 데이터에서는 고정된 글자 수로 자르는 것보다 다루기 쉬운 결과를 얻었습니다.
일본어 장문만을 강력하게 대상으로 한다면, 마침표(句点)나 줄바꿈도 분할 후보에 포함하는 것이 좋습니다.
1회분 요약에서는 출력 언어와 구조를 명시합니다.
private static func summarizeOne(
_ transcript: String,
outputLanguage: String,
...
extraInstruction은 사용자가 "짧게", "중요한 결정 사항 중심으로" 등을 지정하기 위한 것입니다.
다만, 언어나 구조에 대한 지시가 덮어씌워지면 UI가 깨질 수 있으므로, 추가 지시 사항으로서 끝에 덧붙이는 방식으로 처리했습니다.
온디바이스 모델(On-device model)에서는 동일한 문장이나 행을 반복하는 결과가 나올 때가 있었습니다.
이를 그대로 표시하면 요약으로서 사용할 수 없기 때문에, 인접한 동일 행이나 동일 구절을 제거(collapse)하고 있습니다.
static func collapseRepetition(_ text: String) -> String {
func dedupeAdjacent(_ parts: [Substring], join: String) -> String {
var output: [String] = []
...
나아가 전체가 반복으로 지배되어 있는 경우에는 요약 실패로 간주하여 버립니다.
private static func isDegenerate(_ text: String) -> Bool {
let units = text
.split(whereSeparator: { $0 == "\n" || $0 == "。” || $0 == "." })
...
이 판정은 상당히 단순하지만, 동일한 문장을 반복해서 내뱉는 실패 사례를 UI에 노출하지 않는 데 효과가 있었습니다.
호출 측에서는 결과에 따라 UI 상태를 나눕니다.
let outcome = await SummaryService.summarize(
transcript,
outputLanguage: "ja",
...
blocked와 none을 구분해 두면 사용자에게 다음과 같이 서로 다른 설명을 제공할 수 있습니다.
blocked: 내용의 안전성 판정(safety check)으로 인해 요약할 수 없음
none: 너무 짧음, 모델을 사용할 수 없음, 또는 일반적인 실패
FoundationModels를 사용한 요약은 짧은 샘플이라면 respond만으로도 동작합니다.
하지만 동영상 녹취록을 실제 앱에서 다룰 경우에는 다음과 같은 처리를 넣어두면 시스템이 견고해집니다.
SystemLanguageModel.default.availability를 통해 요약 기능만 구분하여 제공- 콘텐츠 변환(content transformation)을 위한 가드레일(guardrails) 선택
text/blocked/none으로 결과 구분- 긴 녹취록은 청크(chunk) 단위로 요약한 후 재요약(re-summarize) 수행
- 생성 결과의 반복을 감지하여 표시하지 않음
특히 긴 동영상의 경우, 한 번의 프롬프트로 모든 것을 처리하려고 하기보다 분할하여 단계적으로 정리하는 것이 구현과 UI 측면 모두에서 더 안정적이었습니다.
관련 기사:
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기