livekit-plugins-volcengine 1.2.0__tar.gz → 1.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/PKG-INFO +2 -2
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/realtime.py +142 -89
- livekit_plugins_volcengine-1.2.3/livekit/plugins/volcengine/version.py +1 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/pyproject.toml +1 -1
- livekit_plugins_volcengine-1.2.0/livekit/plugins/volcengine/version.py +0 -1
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/.gitignore +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/README.md +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/__init__.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/bigmodel_stt.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/llm.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/log.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/py.typed +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/stt.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/tts.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: livekit-plugins-volcengine
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: LiveKit Agent Plugins for Volcengine
|
|
5
5
|
Author-email: wangmengdi <790990241@qq.com>
|
|
6
6
|
Keywords: audio,livekit,realtime,video,webrtc
|
|
@@ -14,7 +14,7 @@ Classifier: Topic :: Multimedia :: Sound/Audio
|
|
|
14
14
|
Classifier: Topic :: Multimedia :: Video
|
|
15
15
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
|
-
Requires-Dist: livekit-agents>=1.2.
|
|
17
|
+
Requires-Dist: livekit-agents>=1.2.3
|
|
18
18
|
Requires-Dist: numpy
|
|
19
19
|
Requires-Dist: openai>=1.75.0
|
|
20
20
|
Requires-Dist: osc-data>=0.2.2
|
|
@@ -15,7 +15,6 @@ from typing import Literal
|
|
|
15
15
|
|
|
16
16
|
import aiohttp
|
|
17
17
|
import numpy as np
|
|
18
|
-
|
|
19
18
|
from livekit import rtc
|
|
20
19
|
from livekit.agents import llm, utils
|
|
21
20
|
from livekit.agents.types import (
|
|
@@ -25,22 +24,8 @@ from livekit.agents.types import (
|
|
|
25
24
|
NotGivenOr,
|
|
26
25
|
)
|
|
27
26
|
|
|
28
|
-
|
|
29
27
|
from .log import logger
|
|
30
28
|
|
|
31
|
-
# When a response is created with the OpenAI Realtime API, those events are sent in this order:
|
|
32
|
-
# 1. response.created (contains resp_id)
|
|
33
|
-
# 2. response.output_item.added (contains item_id)
|
|
34
|
-
# 3. conversation.item.created
|
|
35
|
-
# 4. response.content_part.added (type audio/text)
|
|
36
|
-
# 5. response.audio_transcript.delta (x2, x3, x4, etc)
|
|
37
|
-
# 6. response.audio.delta (x2, x3, x4, etc)
|
|
38
|
-
# 7. response.content_part.done
|
|
39
|
-
# 8. response.output_item.done (contains item_status: "completed/incomplete")
|
|
40
|
-
# 9. response.done (contains status_details for cancelled/failed/turn_detected/content_filter)
|
|
41
|
-
#
|
|
42
|
-
# Ourcode assumes a response will generate only one item with type "message"
|
|
43
|
-
|
|
44
29
|
|
|
45
30
|
PROTOCOL_VERSION = 0b0001
|
|
46
31
|
DEFAULT_HEADER_SIZE = 0b0001
|
|
@@ -186,6 +171,7 @@ class _RealtimeOptions:
|
|
|
186
171
|
conn_options: APIConnectOptions
|
|
187
172
|
opening: str = "你好啊,今天过得怎么样?"
|
|
188
173
|
speaking_style: str = "你的说话风格简洁明了,语速适中,语调自然。"
|
|
174
|
+
speaker: str = "zh_female_vv_jupiter_bigtts"
|
|
189
175
|
sample_rate: int = 24000
|
|
190
176
|
num_channels: int = 1
|
|
191
177
|
format: str = "pcm"
|
|
@@ -204,7 +190,7 @@ class _RealtimeOptions:
|
|
|
204
190
|
}
|
|
205
191
|
return headers
|
|
206
192
|
|
|
207
|
-
def get_start_session_reqs(self):
|
|
193
|
+
def get_start_session_reqs(self, dialog_id: str | None) -> dict:
|
|
208
194
|
start_session_req = {
|
|
209
195
|
"tts": {
|
|
210
196
|
"audio_config": {
|
|
@@ -212,10 +198,12 @@ class _RealtimeOptions:
|
|
|
212
198
|
"format": self.format,
|
|
213
199
|
"sample_rate": self.sample_rate,
|
|
214
200
|
},
|
|
201
|
+
"speaker": self.speaker,
|
|
215
202
|
},
|
|
216
203
|
"dialog": {
|
|
217
204
|
"bot_name": self.bot_name,
|
|
218
205
|
"system_role": self.system_role,
|
|
206
|
+
"dialog_id": dialog_id or str(utils.shortuuid()),
|
|
219
207
|
"speaking_style": self.speaking_style,
|
|
220
208
|
"extra": {"strict_audit": False},
|
|
221
209
|
},
|
|
@@ -249,11 +237,13 @@ class RealtimeModel(llm.RealtimeModel):
|
|
|
249
237
|
def __init__(
|
|
250
238
|
self,
|
|
251
239
|
bot_name: str = "豆包",
|
|
252
|
-
system_role: str = "你是一个语音助手。",
|
|
253
|
-
opening: str = "你好啊,今天过得怎么样?",
|
|
254
240
|
speaking_style: str = "你的说话风格简洁明了,语速适中,语调自然。",
|
|
255
|
-
|
|
256
|
-
|
|
241
|
+
speaker: str = "zh_female_vv_jupiter_bigtts",
|
|
242
|
+
opening: str | None = None,
|
|
243
|
+
app_id: str | None = None,
|
|
244
|
+
access_token: str | None = None,
|
|
245
|
+
system_role: str | None = None,
|
|
246
|
+
audio_output: bool = True,
|
|
257
247
|
http_session: aiohttp.ClientSession | None = None,
|
|
258
248
|
max_session_duration: NotGivenOr[float | None] = NOT_GIVEN,
|
|
259
249
|
conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
|
|
@@ -264,7 +254,7 @@ class RealtimeModel(llm.RealtimeModel):
|
|
|
264
254
|
turn_detection=True,
|
|
265
255
|
user_transcription=True,
|
|
266
256
|
auto_tool_reply_generation=False,
|
|
267
|
-
audio_output=
|
|
257
|
+
audio_output=audio_output,
|
|
268
258
|
)
|
|
269
259
|
)
|
|
270
260
|
|
|
@@ -283,6 +273,7 @@ class RealtimeModel(llm.RealtimeModel):
|
|
|
283
273
|
conn_options=conn_options,
|
|
284
274
|
bot_name=bot_name,
|
|
285
275
|
system_role=system_role,
|
|
276
|
+
speaker=speaker,
|
|
286
277
|
opening=opening,
|
|
287
278
|
speaking_style=speaking_style,
|
|
288
279
|
)
|
|
@@ -349,8 +340,10 @@ class RealtimeSession(
|
|
|
349
340
|
self._current_generation: _ResponseGeneration | None = None
|
|
350
341
|
self._current_item: _MessageGeneration | None = None
|
|
351
342
|
self._remote_chat_ctx = llm.remote_chat_context.RemoteChatContext()
|
|
352
|
-
self._is_opening =
|
|
343
|
+
self._is_opening = False
|
|
353
344
|
self._first_tts_response = True
|
|
345
|
+
self._first_llm_response = True
|
|
346
|
+
self._first_llm_sentence = True
|
|
354
347
|
|
|
355
348
|
self._update_chat_ctx_lock = asyncio.Lock()
|
|
356
349
|
self._update_fnc_ctx_lock = asyncio.Lock()
|
|
@@ -398,6 +391,7 @@ class RealtimeSession(
|
|
|
398
391
|
|
|
399
392
|
async def _run_ws(self, ws_conn: aiohttp.ClientWebSocketResponse) -> None:
|
|
400
393
|
closing = False
|
|
394
|
+
logger.info("start connection")
|
|
401
395
|
start_connection_request = bytearray(generate_header())
|
|
402
396
|
start_connection_request.extend(int(1).to_bytes(4, "big"))
|
|
403
397
|
payload_bytes = str.encode("{}")
|
|
@@ -406,64 +400,55 @@ class RealtimeSession(
|
|
|
406
400
|
start_connection_request.extend(payload_bytes)
|
|
407
401
|
await ws_conn.send_bytes(start_connection_request)
|
|
408
402
|
_ = await ws_conn.receive_bytes()
|
|
409
|
-
logger.info("startConnection response")
|
|
410
403
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
self._current_generation = _ResponseGeneration(
|
|
440
|
-
message_ch=utils.aio.Chan(),
|
|
441
|
-
function_ch=utils.aio.Chan(),
|
|
442
|
-
messages={},
|
|
443
|
-
_created_timestamp=time.time(),
|
|
444
|
-
_done_fut=asyncio.Future(),
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
generation_ev = llm.GenerationCreatedEvent(
|
|
448
|
-
message_stream=self._current_generation.message_ch,
|
|
449
|
-
function_stream=self._current_generation.function_ch,
|
|
450
|
-
user_initiated=False,
|
|
451
|
-
)
|
|
452
|
-
self.emit("generation_created", generation_ev)
|
|
453
|
-
item_id = utils.shortuuid()
|
|
454
|
-
self._current_item = _MessageGeneration(
|
|
455
|
-
message_id=item_id,
|
|
456
|
-
text_ch=utils.aio.Chan(),
|
|
457
|
-
audio_ch=utils.aio.Chan(),
|
|
458
|
-
)
|
|
404
|
+
logger.info("start session")
|
|
405
|
+
await self._start_session(ws_conn=ws_conn, dialog_id=self.session_id)
|
|
406
|
+
|
|
407
|
+
if self._realtime_model._opts.opening is not None:
|
|
408
|
+
self._is_opening = True
|
|
409
|
+
payload = {
|
|
410
|
+
"content": self._realtime_model._opts.opening,
|
|
411
|
+
}
|
|
412
|
+
hello_request = bytearray(generate_header())
|
|
413
|
+
hello_request.extend(int(300).to_bytes(4, "big"))
|
|
414
|
+
payload_bytes = str.encode(json.dumps(payload))
|
|
415
|
+
payload_bytes = gzip.compress(payload_bytes)
|
|
416
|
+
hello_request.extend((len(self.session_id)).to_bytes(4, "big"))
|
|
417
|
+
hello_request.extend(str.encode(self.session_id))
|
|
418
|
+
hello_request.extend((len(payload_bytes)).to_bytes(4, "big"))
|
|
419
|
+
hello_request.extend(payload_bytes)
|
|
420
|
+
await ws_conn.send_bytes(hello_request)
|
|
421
|
+
self._is_opening = True
|
|
422
|
+
logger.info("send hello request")
|
|
423
|
+
|
|
424
|
+
self._current_generation = _ResponseGeneration(
|
|
425
|
+
message_ch=utils.aio.Chan(),
|
|
426
|
+
function_ch=utils.aio.Chan(),
|
|
427
|
+
messages={},
|
|
428
|
+
_created_timestamp=time.time(),
|
|
429
|
+
_done_fut=asyncio.Future(),
|
|
430
|
+
)
|
|
459
431
|
|
|
460
|
-
|
|
461
|
-
|
|
432
|
+
generation_ev = llm.GenerationCreatedEvent(
|
|
433
|
+
message_stream=self._current_generation.message_ch,
|
|
434
|
+
function_stream=self._current_generation.function_ch,
|
|
435
|
+
user_initiated=False,
|
|
436
|
+
)
|
|
437
|
+
self.emit("generation_created", generation_ev)
|
|
438
|
+
item_id = utils.shortuuid()
|
|
439
|
+
self._current_item = _MessageGeneration(
|
|
462
440
|
message_id=item_id,
|
|
463
|
-
|
|
464
|
-
|
|
441
|
+
text_ch=utils.aio.Chan(),
|
|
442
|
+
audio_ch=utils.aio.Chan(),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
self._current_generation.message_ch.send_nowait(
|
|
446
|
+
llm.MessageGeneration(
|
|
447
|
+
message_id=item_id,
|
|
448
|
+
text_stream=self._current_item.text_ch,
|
|
449
|
+
audio_stream=self._current_item.audio_ch,
|
|
450
|
+
)
|
|
465
451
|
)
|
|
466
|
-
)
|
|
467
452
|
|
|
468
453
|
@utils.log_exceptions(logger=logger)
|
|
469
454
|
async def _send_task() -> None:
|
|
@@ -504,7 +489,7 @@ class RealtimeSession(
|
|
|
504
489
|
event = response.get("event")
|
|
505
490
|
if event == 450: # ASRInfo
|
|
506
491
|
self.emit("input_speech_started", llm.InputSpeechStartedEvent())
|
|
507
|
-
logger.info("
|
|
492
|
+
logger.info("transcription start")
|
|
508
493
|
elif event == 451: # ASRResponse
|
|
509
494
|
response = response["payload_msg"]
|
|
510
495
|
transcription = response["results"][0]["alternatives"][0][
|
|
@@ -552,16 +537,19 @@ class RealtimeSession(
|
|
|
552
537
|
)
|
|
553
538
|
|
|
554
539
|
elif event == 459: # ASREnd
|
|
555
|
-
logger.info("
|
|
540
|
+
logger.info("transcription end")
|
|
556
541
|
self.emit(
|
|
557
542
|
"input_speech_stopped",
|
|
558
543
|
llm.InputSpeechStoppedEvent(
|
|
559
544
|
user_transcription_enabled=False
|
|
560
545
|
),
|
|
561
546
|
)
|
|
547
|
+
logger.info("llm start")
|
|
548
|
+
logger.info("tts start")
|
|
562
549
|
|
|
563
550
|
elif event == 352: # TTSResponse
|
|
564
551
|
if self._first_tts_response:
|
|
552
|
+
logger.info("llm first sentence")
|
|
565
553
|
logger.info("tts first response")
|
|
566
554
|
self._first_tts_response = False
|
|
567
555
|
audio_bytes = response[
|
|
@@ -580,7 +568,12 @@ class RealtimeSession(
|
|
|
580
568
|
samples_per_channel=len(audio_bytes) // 2,
|
|
581
569
|
)
|
|
582
570
|
)
|
|
571
|
+
elif event == 350: # TTSSentenceStart
|
|
572
|
+
pass
|
|
573
|
+
elif event == 351: # TTSSentenceEnd
|
|
574
|
+
pass
|
|
583
575
|
elif event == 359: # TTSEnded
|
|
576
|
+
logger.info("tts end")
|
|
584
577
|
self._current_item.audio_ch.close()
|
|
585
578
|
if self._is_opening:
|
|
586
579
|
self._current_item.text_ch.send_nowait(
|
|
@@ -593,10 +586,15 @@ class RealtimeSession(
|
|
|
593
586
|
self._current_generation = None
|
|
594
587
|
self._first_tts_response = True
|
|
595
588
|
elif event == 550: # 模型回复的文本内容
|
|
589
|
+
if self._first_llm_response:
|
|
590
|
+
logger.info("llm first response")
|
|
591
|
+
self._first_llm_response = False
|
|
596
592
|
text = response["payload_msg"]["content"]
|
|
597
593
|
self._current_item.text_ch.send_nowait(text)
|
|
598
594
|
elif event == 559: # 模型回复文本结束事件
|
|
595
|
+
logger.info("llm end")
|
|
599
596
|
self._current_item.text_ch.close()
|
|
597
|
+
self._first_llm_response = True
|
|
600
598
|
else:
|
|
601
599
|
pass
|
|
602
600
|
except Exception:
|
|
@@ -607,20 +605,10 @@ class RealtimeSession(
|
|
|
607
605
|
asyncio.create_task(_recv_task(), name="_recv_task"),
|
|
608
606
|
asyncio.create_task(_send_task(), name="_send_task"),
|
|
609
607
|
]
|
|
610
|
-
wait_reconnect_task: asyncio.Task | None = None
|
|
611
|
-
# if self._realtime_model._opts.max_session_duration is not None:
|
|
612
|
-
# wait_reconnect_task = asyncio.create_task(
|
|
613
|
-
# asyncio.sleep(self._realtime_model._opts.max_session_duration),
|
|
614
|
-
# name="_timeout_task",
|
|
615
|
-
# )
|
|
616
|
-
# tasks.append(wait_reconnect_task)
|
|
617
608
|
try:
|
|
618
609
|
done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
619
|
-
|
|
620
|
-
# propagate exceptions from completed tasks
|
|
621
610
|
for task in done:
|
|
622
|
-
|
|
623
|
-
task.result()
|
|
611
|
+
task.result()
|
|
624
612
|
|
|
625
613
|
finally:
|
|
626
614
|
await utils.aio.cancel_and_wait(*tasks)
|
|
@@ -629,6 +617,71 @@ class RealtimeSession(
|
|
|
629
617
|
def _create_session_update_event(self):
|
|
630
618
|
pass
|
|
631
619
|
|
|
620
|
+
async def chat_tts_text(
|
|
621
|
+
self,
|
|
622
|
+
start: bool,
|
|
623
|
+
end: bool,
|
|
624
|
+
content: str,
|
|
625
|
+
ws_conn: aiohttp.ClientWebSocketResponse,
|
|
626
|
+
) -> None:
|
|
627
|
+
"""发送Chat TTS Text消息"""
|
|
628
|
+
payload = {
|
|
629
|
+
"start": start,
|
|
630
|
+
"end": end,
|
|
631
|
+
"content": content,
|
|
632
|
+
}
|
|
633
|
+
logger.info("ChatTTSTextRequest")
|
|
634
|
+
payload_bytes = str.encode(json.dumps(payload))
|
|
635
|
+
payload_bytes = gzip.compress(payload_bytes)
|
|
636
|
+
|
|
637
|
+
chat_tts_text_request = bytearray(generate_header())
|
|
638
|
+
chat_tts_text_request.extend(int(500).to_bytes(4, "big"))
|
|
639
|
+
chat_tts_text_request.extend((len(self.session_id)).to_bytes(4, "big"))
|
|
640
|
+
chat_tts_text_request.extend(str.encode(self.session_id))
|
|
641
|
+
chat_tts_text_request.extend((len(payload_bytes)).to_bytes(4, "big"))
|
|
642
|
+
chat_tts_text_request.extend(payload_bytes)
|
|
643
|
+
await ws_conn.send_bytes(chat_tts_text_request)
|
|
644
|
+
|
|
645
|
+
async def _start_session(
|
|
646
|
+
self, ws_conn: aiohttp.ClientWebSocketResponse, dialog_id: str
|
|
647
|
+
) -> None:
|
|
648
|
+
request_params = self._realtime_model._opts.get_start_session_reqs(
|
|
649
|
+
dialog_id=dialog_id
|
|
650
|
+
)
|
|
651
|
+
payload_bytes = str.encode(json.dumps(request_params))
|
|
652
|
+
payload_bytes = gzip.compress(payload_bytes)
|
|
653
|
+
start_session_request = bytearray(generate_header())
|
|
654
|
+
start_session_request.extend(int(100).to_bytes(4, "big"))
|
|
655
|
+
start_session_request.extend((len(self.session_id)).to_bytes(4, "big"))
|
|
656
|
+
start_session_request.extend(str.encode(self.session_id))
|
|
657
|
+
start_session_request.extend((len(payload_bytes)).to_bytes(4, "big"))
|
|
658
|
+
start_session_request.extend(payload_bytes)
|
|
659
|
+
await ws_conn.send_bytes(start_session_request)
|
|
660
|
+
_ = await ws_conn.receive_bytes()
|
|
661
|
+
|
|
662
|
+
async def _finish_session(self, ws_conn: aiohttp.ClientWebSocketResponse) -> None:
|
|
663
|
+
finish_session_request = bytearray(generate_header())
|
|
664
|
+
finish_session_request.extend(int(102).to_bytes(4, "big"))
|
|
665
|
+
payload_bytes = str.encode("{}")
|
|
666
|
+
payload_bytes = gzip.compress(payload_bytes)
|
|
667
|
+
finish_session_request.extend((len(self.session_id)).to_bytes(4, "big"))
|
|
668
|
+
finish_session_request.extend(str.encode(self.session_id))
|
|
669
|
+
finish_session_request.extend((len(payload_bytes)).to_bytes(4, "big"))
|
|
670
|
+
finish_session_request.extend(payload_bytes)
|
|
671
|
+
await ws_conn.send_bytes(finish_session_request)
|
|
672
|
+
|
|
673
|
+
async def _finish_connection(
|
|
674
|
+
self, ws_conn: aiohttp.ClientWebSocketResponse
|
|
675
|
+
) -> None:
|
|
676
|
+
finish_connection_request = bytearray(generate_header())
|
|
677
|
+
finish_connection_request.extend(int(2).to_bytes(4, "big"))
|
|
678
|
+
payload_bytes = str.encode("{}")
|
|
679
|
+
payload_bytes = gzip.compress(payload_bytes)
|
|
680
|
+
finish_connection_request.extend((len(payload_bytes)).to_bytes(4, "big"))
|
|
681
|
+
finish_connection_request.extend(payload_bytes)
|
|
682
|
+
await ws_conn.send_bytes(finish_connection_request)
|
|
683
|
+
_ = await ws_conn.receive_bytes()
|
|
684
|
+
|
|
632
685
|
@property
|
|
633
686
|
def chat_ctx(self) -> llm.ChatContext:
|
|
634
687
|
return self._remote_chat_ctx.to_chat_ctx()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.3"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|