33
44namespace ProjectVG . Application . Models . Chat
55{
6- public record ChatSegment
6+ public sealed class ChatSegment : IDisposable
77 {
88
9- public string Content { get ; init ; } = string . Empty ;
9+ public string Content { get ; private set ; } = string . Empty ;
1010
11- public int Order { get ; init ; }
11+ public int Order { get ; private set ; }
1212
13- public string ? Emotion { get ; init ; }
13+ public string ? Emotion { get ; private set ; }
1414
15- public List < string > ? Actions { get ; init ; }
15+ public List < string > ? Actions { get ; private set ; }
1616
17- public byte [ ] ? AudioData { get ; init ; }
18- public string ? AudioContentType { get ; init ; }
19- public float ? AudioLength { get ; init ; }
17+ public byte [ ] ? AudioData { get ; private set ; }
18+ public string ? AudioContentType { get ; private set ; }
19+ public float ? AudioLength { get ; private set ; }
2020
21- // 스트림 기반 음성 데이터 처리를 위한 새로운 프로퍼티
22- public IMemoryOwner < byte > ? AudioMemoryOwner { get ; init ; }
23- public int AudioDataSize { get ; init ; }
21+ // LOH 방지를 위한 ArrayPool 기반 메모리 관리
22+ internal IMemoryOwner < byte > ? AudioMemoryOwner { get ; private set ; }
23+ internal int AudioDataSize { get ; private set ; }
24+ private bool _disposed ;
2425
2526
2627
@@ -30,14 +31,13 @@ public record ChatSegment
3031 public bool HasEmotion => ! string . IsNullOrEmpty ( Emotion ) ;
3132 public bool HasActions => Actions != null && Actions . Any ( ) ;
3233
33- /// <summary>
34- /// 메모리 효율적인 방식으로 음성 데이터에 접근합니다
35- /// </summary>
3634 public ReadOnlySpan < byte > GetAudioSpan ( )
3735 {
3836 if ( AudioMemoryOwner != null && AudioDataSize > 0 )
3937 {
40- return AudioMemoryOwner . Memory . Span . Slice ( 0 , AudioDataSize ) ;
38+ var memory = AudioMemoryOwner . Memory ;
39+ var safeSize = Math . Min ( AudioDataSize , memory . Length ) ;
40+ return memory . Span . Slice ( 0 , safeSize ) ;
4141 }
4242 if ( AudioData != null )
4343 {
@@ -48,6 +48,8 @@ public ReadOnlySpan<byte> GetAudioSpan()
4848
4949
5050
51+ private ChatSegment ( ) { }
52+
5153 public static ChatSegment Create ( string content , string ? emotion = null , List < string > ? actions = null , int order = 0 )
5254 {
5355 return new ChatSegment
@@ -69,36 +71,80 @@ public static ChatSegment CreateAction(string action, int order = 0)
6971 return Create ( "" , null , new List < string > { action } , order ) ;
7072 }
7173
72- // Method to add audio data (returns new record instance)
7374 public ChatSegment WithAudioData ( byte [ ] audioData , string audioContentType , float audioLength )
7475 {
75- return this with
76+ return new ChatSegment
7677 {
78+ Content = this . Content ,
79+ Order = this . Order ,
80+ Emotion = this . Emotion ,
81+ Actions = this . Actions ,
7782 AudioData = audioData ,
7883 AudioContentType = audioContentType ,
7984 AudioLength = audioLength
8085 } ;
8186 }
8287
83- /// <summary>
84- /// 메모리 효율적인 방식으로 음성 데이터를 추가합니다 (LOH 방지)
85- /// </summary>
88+ // 주의: 원본 인스턴스의 AudioMemoryOwner 해제됨
8689 public ChatSegment WithAudioMemory ( IMemoryOwner < byte > audioMemoryOwner , int audioDataSize , string audioContentType , float audioLength )
8790 {
88- return this with
91+ if ( audioMemoryOwner is null )
92+ throw new ArgumentNullException ( nameof ( audioMemoryOwner ) ) ;
93+
94+ if ( audioDataSize < 0 || audioDataSize > audioMemoryOwner . Memory . Length )
95+ throw new ArgumentOutOfRangeException (
96+ nameof ( audioDataSize ) ,
97+ audioDataSize ,
98+ $ "audioDataSize는 0 이상 { audioMemoryOwner . Memory . Length } 이하여야 합니다.") ;
99+
100+ // 기존 소유자 해제 및 상태 정리
101+ this . AudioMemoryOwner ? . Dispose ( ) ;
102+ this . AudioMemoryOwner = null ;
103+ this . AudioDataSize = 0 ;
104+
105+ return new ChatSegment
89106 {
107+ Content = this . Content ,
108+ Order = this . Order ,
109+ Emotion = this . Emotion ,
110+ Actions = this . Actions ,
90111 AudioMemoryOwner = audioMemoryOwner ,
91112 AudioDataSize = audioDataSize ,
92113 AudioContentType = audioContentType ,
93114 AudioLength = audioLength ,
94- // 기존 AudioData는 null로 설정하여 중복 저장 방지
95115 AudioData = null
96116 } ;
97117 }
98118
99119 /// <summary>
100- /// 음성 데이터를 배열로 변환합니다 (필요한 경우에만 사용 )
120+ /// 오디오 메모리를 부착한 새 인스턴스 생성 (원본 불변 )
101121 /// </summary>
122+ public ChatSegment AttachAudioMemory ( IMemoryOwner < byte > audioMemoryOwner , int audioDataSize , string audioContentType , float audioLength )
123+ {
124+ if ( audioMemoryOwner is null )
125+ throw new ArgumentNullException ( nameof ( audioMemoryOwner ) ) ;
126+
127+ if ( audioDataSize < 0 || audioDataSize > audioMemoryOwner . Memory . Length )
128+ throw new ArgumentOutOfRangeException (
129+ nameof ( audioDataSize ) ,
130+ audioDataSize ,
131+ $ "audioDataSize는 0 이상 { audioMemoryOwner . Memory . Length } 이하여야 합니다.") ;
132+
133+ return new ChatSegment
134+ {
135+ Content = this . Content ,
136+ Order = this . Order ,
137+ Emotion = this . Emotion ,
138+ Actions = this . Actions ,
139+ AudioMemoryOwner = audioMemoryOwner ,
140+ AudioDataSize = audioDataSize ,
141+ AudioContentType = audioContentType ,
142+ AudioLength = audioLength ,
143+ AudioData = null
144+ } ;
145+ }
146+
147+ // 필요시만 사용 - LOH 위험 있음
102148 public byte [ ] ? GetAudioDataAsArray ( )
103149 {
104150 if ( AudioData != null )
@@ -115,12 +161,11 @@ public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audi
115161 return null ;
116162 }
117163
118- /// <summary>
119- /// 리소스 해제 (IMemoryOwner 해제)
120- /// </summary>
121164 public void Dispose ( )
122165 {
166+ if ( _disposed ) return ;
123167 AudioMemoryOwner ? . Dispose ( ) ;
168+ _disposed = true ;
124169 }
125170 }
126171}
0 commit comments