Quang Long commited on
Commit
1dce2dd
·
1 Parent(s): a3fd3c7

update docker-compose, dockerfile, setup cronjob, fix generate audio

Browse files
.dockerignore CHANGED
@@ -1,3 +1,5 @@
1
  .python-version
2
  .backup
3
  backup
 
 
 
1
  .python-version
2
  .backup
3
  backup
4
+ results/
5
+ tts_cache/
Dockerfile CHANGED
@@ -26,6 +26,7 @@ RUN apt-get update && \
26
  libxmlsec1-dev \
27
  libffi-dev \
28
  liblzma-dev && \
 
29
  apt-get clean && \
30
  rm -rf /var/lib/apt/lists/*
31
 
@@ -48,6 +49,7 @@ COPY --chown=1000 requirements.txt /tmp/requirements.txt
48
  RUN pip install --no-cache-dir -U -r /tmp/requirements.txt
49
 
50
  COPY --chown=1000 . ${HOME}/app
 
51
  RUN ls -a
52
  ENV PYTHONPATH=${HOME}/app \
53
  PYTHONUNBUFFERED=1 \
@@ -56,4 +58,7 @@ ENV PYTHONPATH=${HOME}/app \
56
  GRADIO_SERVER_NAME=0.0.0.0 \
57
  GRADIO_THEME=huggingface \
58
  SYSTEM=spaces
 
 
 
59
  CMD ["python", "app.py"]
 
26
  libxmlsec1-dev \
27
  libffi-dev \
28
  liblzma-dev && \
29
+ apt-get install -y cron && \
30
  apt-get clean && \
31
  rm -rf /var/lib/apt/lists/*
32
 
 
49
  RUN pip install --no-cache-dir -U -r /tmp/requirements.txt
50
 
51
  COPY --chown=1000 . ${HOME}/app
52
+ RUN chmod +x ${HOME}/app/utils/clear_results.sh ${HOME}/app/utils/entrypoint.sh
53
  RUN ls -a
54
  ENV PYTHONPATH=${HOME}/app \
55
  PYTHONUNBUFFERED=1 \
 
58
  GRADIO_SERVER_NAME=0.0.0.0 \
59
  GRADIO_THEME=huggingface \
60
  SYSTEM=spaces
61
+
62
+ USER root
63
+ ENTRYPOINT ["/home/user/app/utils/entrypoint.sh"]
64
  CMD ["python", "app.py"]
README.md CHANGED
@@ -1,49 +1,107 @@
 
 
 
 
 
 
 
1
  ---
2
- title: SadTalker
 
3
  emoji: 😭
4
  colorFrom: purple
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.45.0
8
  python_version: 3.10.18
9
  app_file: app.py
10
  pinned: false
11
  license: mit
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
15
 
16
- Use local:
17
- python=3.10
18
 
19
- ```
20
- pip install torch==2.4.0+cu124 torchaudio==2.4.0+cu124 torchvision==0.19.0 --extra-index-url https://download.pytorch.org/whl/cu124
 
 
 
 
 
 
 
21
  ```
22
 
 
 
 
 
23
  ```
 
 
 
 
24
  sudo apt-get update
25
  sudo apt-get install sox ffmpeg
26
  ```
27
 
28
- ## Backup and Restore Volume
 
 
29
 
30
- p/s: Need to mount backup folder to container
31
 
32
- ### Backup
33
 
34
  ```bash
 
35
  tar -czvf /backup/data_cache_backup.tar.gz /home/user/.cache
 
 
36
  tar -czvf /backup/data_gfpgan_backup.tar.gz /home/user/app/gfpgan
37
  ```
38
 
39
- ### Restore
40
 
41
  ```bash
 
42
  mkdir -p /home/user/.cache
43
  cd /home/user/.cache
44
  tar -xzvf /backup/data_cache_backup.tar.gz --strip 1
45
 
 
46
  mkdir -p /home/user/app/gfpgan
47
  cd /home/user/app/gfpgan
48
  tar -xzvf /backup/data_gfpgan_backup.tar.gz --strip 1
49
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 😭 Atalink-TTS-Talker
2
+
3
+ A Hugging Face Space powered by **Gradio**.
4
+ This project demonstrates **SadTalker** with local environment setup, backup/restore guides, and Docker deployment.
5
+
6
+ ## ⚙️ Configuration (for Hugging Face Spaces)
7
+
8
  ---
9
+
10
+ title: Atalink-TTS-Talker
11
  emoji: 😭
12
  colorFrom: purple
13
+ colorTo: blue
14
  sdk: gradio
15
  sdk_version: 5.45.0
16
  python_version: 3.10.18
17
  app_file: app.py
18
  pinned: false
19
  license: mit
20
+
21
  ---
22
 
23
+ ## Reference: [Spaces Config Docs](https://huggingface.co/docs/hub/spaces-config-reference)
24
+
25
+ ---
26
 
27
+ ### Use local:
 
28
 
29
+ ## 🖥️ Local Setup
30
+
31
+ ### 1. Python environment
32
+
33
+ - Use **Python 3.10**
34
+
35
+ ```bash
36
+ python -m venv .venv
37
+ source .venv/bin/activate
38
  ```
39
 
40
+ ### 2. Install PyTorch with CUDA 12.4
41
+
42
+ ```bash
43
+ pip install torch==2.4.0+cu124 torchaudio==2.4.0+cu124 torchvision==0.19.0 --extra-index-url https://download.pytorch.org/whl/cu124
44
  ```
45
+
46
+ ### 3. Install dependencies
47
+
48
+ ```bash
49
  sudo apt-get update
50
  sudo apt-get install sox ffmpeg
51
  ```
52
 
53
+ ---
54
+
55
+ ## 💾 Backup & Restore Volumes
56
 
57
+ > ⚠️ Make sure you **mount the backup folder** into the container before running these commands.
58
 
59
+ ### 🔹 Backup
60
 
61
  ```bash
62
+ # Cache
63
  tar -czvf /backup/data_cache_backup.tar.gz /home/user/.cache
64
+
65
+ # GFPGAN data
66
  tar -czvf /backup/data_gfpgan_backup.tar.gz /home/user/app/gfpgan
67
  ```
68
 
69
+ ### 🔹 Restore
70
 
71
  ```bash
72
+ # Restore cache
73
  mkdir -p /home/user/.cache
74
  cd /home/user/.cache
75
  tar -xzvf /backup/data_cache_backup.tar.gz --strip 1
76
 
77
+ # Restore GFPGAN
78
  mkdir -p /home/user/app/gfpgan
79
  cd /home/user/app/gfpgan
80
  tar -xzvf /backup/data_gfpgan_backup.tar.gz --strip 1
81
  ```
82
+
83
+ ---
84
+
85
+ ## 🚀 Running the App
86
+
87
+ 1. Create and activate Python 3.10 environment
88
+ 2. Prepare environment:
89
+
90
+ ```bash
91
+ python utils/prepare_environment.py
92
+ ```
93
+
94
+ 3. Start with Docker Compose:
95
+
96
+ ```bash
97
+ docker compose up -d
98
+ ```
99
+
100
+ 4. If you change code, rebuild:
101
+ ```bash
102
+ docker compose up -d --build
103
+ ```
104
+
105
+ ---
106
+
107
+ ✨ Done! You can now run **SadTalker** locally or deploy with Docker.
app.py CHANGED
@@ -46,7 +46,8 @@ def download_model():
46
 
47
  def list_videos():
48
  # Lấy danh sách tất cả file mp4 trong results
49
- video_files = glob.glob("results/**/*.mp4", recursive=True)
 
50
  # Trả về danh sách file (có thể sort theo thời gian)
51
  return sorted(video_files, reverse=True)
52
 
@@ -77,23 +78,27 @@ def generate_voice_and_video(
77
  blink_every,
78
  ):
79
  import gradio as gr
 
80
  # Bắt đầu: Hiển thị trạng thái đang tạo audio
81
  yield (
82
  gr.update(value=None, visible=True, interactive=False),
83
  gr.update(value=None, visible=True, interactive=False),
84
- gr.update(value="⏳ Đang tạo âm thanh...", visible=True)
 
85
  )
86
 
87
  # 1. Sinh audio từ TTS
88
  (final_sample_rate, final_wave), _ = infer_tts(ref_audio, ref_text, gen_text, speed)
89
  tmp_audio = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
90
  import soundfile as sf
 
91
  sf.write(tmp_audio.name, final_wave, final_sample_rate)
92
  # Audio xong, chuyển sang tạo video
93
  yield (
94
  gr.update(value=tmp_audio.name, visible=True, interactive=True),
95
  gr.update(value=None, visible=True, interactive=False),
96
- gr.update(value="⏳ Đang tạo video...", visible=True)
 
97
  )
98
 
99
  # 2. Gọi SadTalker với audio vừa sinh ra
@@ -120,7 +125,8 @@ def generate_voice_and_video(
120
  yield (
121
  gr.update(value=tmp_audio.name, visible=True, interactive=True),
122
  gr.update(value=video_path, visible=True, interactive=True),
123
- gr.update(value="✅ Hoàn thành!", visible=True)
 
124
  )
125
 
126
 
@@ -228,7 +234,12 @@ def sadtalker_demo():
228
  gen_video = gr.Video(
229
  label="Video đã tạo", format="mp4", scale=1, width=180
230
  )
231
- status_box = gr.Textbox(label="Trạng thái tiến trình", interactive=False, value="", visible=True)
 
 
 
 
 
232
 
233
  def enable_generate(audio, text, image):
234
  return gr.update(interactive=bool(audio and text and image))
@@ -243,34 +254,10 @@ def sadtalker_demo():
243
  enable_generate, [ref_audio, gen_text, source_image], btn_generate
244
  )
245
 
246
- btn_generate.click(
247
- generate_voice_and_video,
248
- inputs=[
249
- ref_audio,
250
- ref_text,
251
- gen_text,
252
- speed,
253
- source_image,
254
- preprocess_type,
255
- is_still_mode,
256
- enhancer,
257
- batch_size,
258
- size_of_image,
259
- pose_style,
260
- facerender,
261
- exp_weight,
262
- use_ref_video,
263
- ref_video,
264
- ref_info,
265
- use_idle_mode,
266
- length_of_audio,
267
- blink_every,
268
- ],
269
- outputs=[output_audio, gen_video, status_box],
270
- )
271
  with gr.Tab("Lịch sử video"):
272
  with gr.Row(elem_classes="gr-row"):
273
  video_list = gr.Dropdown(
 
274
  choices=list_videos(),
275
  label="Chọn video để xem",
276
  interactive=True,
@@ -281,6 +268,31 @@ def sadtalker_demo():
281
  )
282
  video_list.change(lambda x: x, inputs=video_list, outputs=video_player)
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  return sadtalker_interface
285
 
286
 
 
46
 
47
  def list_videos():
48
  # Lấy danh sách tất cả file mp4 trong results
49
+ PATH_RESULTS = "results"
50
+ video_files = glob.glob(f"{PATH_RESULTS}/**/*.mp4", recursive=True)
51
  # Trả về danh sách file (có thể sort theo thời gian)
52
  return sorted(video_files, reverse=True)
53
 
 
78
  blink_every,
79
  ):
80
  import gradio as gr
81
+
82
  # Bắt đầu: Hiển thị trạng thái đang tạo audio
83
  yield (
84
  gr.update(value=None, visible=True, interactive=False),
85
  gr.update(value=None, visible=True, interactive=False),
86
+ gr.update(value="⏳ Đang tạo âm thanh...", visible=True),
87
+ gr.update(choices=list_videos()),
88
  )
89
 
90
  # 1. Sinh audio từ TTS
91
  (final_sample_rate, final_wave), _ = infer_tts(ref_audio, ref_text, gen_text, speed)
92
  tmp_audio = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
93
  import soundfile as sf
94
+
95
  sf.write(tmp_audio.name, final_wave, final_sample_rate)
96
  # Audio xong, chuyển sang tạo video
97
  yield (
98
  gr.update(value=tmp_audio.name, visible=True, interactive=True),
99
  gr.update(value=None, visible=True, interactive=False),
100
+ gr.update(value="⏳ Đang tạo video...", visible=True),
101
+ gr.update(choices=list_videos()),
102
  )
103
 
104
  # 2. Gọi SadTalker với audio vừa sinh ra
 
125
  yield (
126
  gr.update(value=tmp_audio.name, visible=True, interactive=True),
127
  gr.update(value=video_path, visible=True, interactive=True),
128
+ gr.update(value="✅ Hoàn thành!", visible=True),
129
+ gr.update(choices=list_videos(), value=video_path),
130
  )
131
 
132
 
 
234
  gen_video = gr.Video(
235
  label="Video đã tạo", format="mp4", scale=1, width=180
236
  )
237
+ status_box = gr.Textbox(
238
+ label="Trạng thái tiến trình",
239
+ interactive=False,
240
+ value="",
241
+ visible=True,
242
+ )
243
 
244
  def enable_generate(audio, text, image):
245
  return gr.update(interactive=bool(audio and text and image))
 
254
  enable_generate, [ref_audio, gen_text, source_image], btn_generate
255
  )
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  with gr.Tab("Lịch sử video"):
258
  with gr.Row(elem_classes="gr-row"):
259
  video_list = gr.Dropdown(
260
+ value=list_videos()[0] if len(list_videos()) > 0 else None,
261
  choices=list_videos(),
262
  label="Chọn video để xem",
263
  interactive=True,
 
268
  )
269
  video_list.change(lambda x: x, inputs=video_list, outputs=video_player)
270
 
271
+ btn_generate.click(
272
+ generate_voice_and_video,
273
+ inputs=[
274
+ ref_audio,
275
+ ref_text,
276
+ gen_text,
277
+ speed,
278
+ source_image,
279
+ preprocess_type,
280
+ is_still_mode,
281
+ enhancer,
282
+ batch_size,
283
+ size_of_image,
284
+ pose_style,
285
+ facerender,
286
+ exp_weight,
287
+ use_ref_video,
288
+ ref_video,
289
+ ref_info,
290
+ use_idle_mode,
291
+ length_of_audio,
292
+ blink_every,
293
+ ],
294
+ outputs=[output_audio, gen_video, status_box, video_list],
295
+ )
296
  return sadtalker_interface
297
 
298
 
app_tts.py CHANGED
@@ -34,6 +34,7 @@ from pathlib import Path
34
  from omegaconf import OmegaConf
35
  from datetime import datetime
36
  import hashlib
 
37
  # Retrieve token from secrets
38
  hf_token = os.getenv("HUGGINGFACEHUB_API_TOKEN")
39
 
@@ -173,7 +174,8 @@ def infer_tts(ref_audio_orig: str, ref_text_input: str, gen_text: str, speed: fl
173
  try:
174
  # Nếu người dùng nhập ref_text thì dùng, không thì để rỗng để tự động nhận diện
175
  ref_audio, ref_text = preprocess_ref_audio_text(ref_audio_orig, ref_text_input or "")
176
- gen_text_ = gen_text.strip()
 
177
  # --- BẮT ĐẦU: Thêm logic cache ---
178
  cache_path = get_audio_cache_path(gen_text_, ref_audio_orig, model)
179
  import soundfile as sf
@@ -183,7 +185,7 @@ def infer_tts(ref_audio_orig: str, ref_text_input: str, gen_text: str, speed: fl
183
  spectrogram = None
184
  else:
185
  final_wave, final_sample_rate, spectrogram = infer_process(
186
- ref_audio, ref_text.lower(), gen_text_, ema_model, vocoder, speed=speed
187
  )
188
  print(f"[CACHE] Saved new audio to: {cache_path}")
189
  sf.write(cache_path, final_wave, final_sample_rate)
 
34
  from omegaconf import OmegaConf
35
  from datetime import datetime
36
  import hashlib
37
+ import unicodedata
38
  # Retrieve token from secrets
39
  hf_token = os.getenv("HUGGINGFACEHUB_API_TOKEN")
40
 
 
174
  try:
175
  # Nếu người dùng nhập ref_text thì dùng, không thì để rỗng để tự động nhận diện
176
  ref_audio, ref_text = preprocess_ref_audio_text(ref_audio_orig, ref_text_input or "")
177
+ ref_text = unicodedata.normalize("NFC", ref_text.strip())
178
+ gen_text_ = unicodedata.normalize("NFC", gen_text.strip())
179
  # --- BẮT ĐẦU: Thêm logic cache ---
180
  cache_path = get_audio_cache_path(gen_text_, ref_audio_orig, model)
181
  import soundfile as sf
 
185
  spectrogram = None
186
  else:
187
  final_wave, final_sample_rate, spectrogram = infer_process(
188
+ ref_audio, ref_text, gen_text_, ema_model, vocoder, speed=speed
189
  )
190
  print(f"[CACHE] Saved new audio to: {cache_path}")
191
  sf.write(cache_path, final_wave, final_sample_rate)
docker-compose.yaml CHANGED
@@ -1,20 +1,25 @@
1
  services:
2
- test:
3
  build: .
4
  ports:
5
- - "7860:7860"
6
- stdin_open: true # equivalent to -it
7
- tty: true # equivalent to -it
8
- restart: "no" # equivalent to --rm (don’t restart container automatically)
9
- # environment:
10
- # - HF_ENDPOINT=http://172.16.15.118:9557/repository/atalink-hf-models
 
11
  volumes:
12
- - test_data_cache:/home/user/.cache
13
- - test_data_gfpgan:/home/user/app/gfpgan
14
- # - ./backup:/backup
15
-
 
 
 
 
16
  volumes:
17
- test_data_cache:
18
  external: true
19
- test_data_gfpgan:
20
  external: true
 
1
  services:
2
+ atalink-tts-talker:
3
  build: .
4
  ports:
5
+ - '7860:7860'
6
+ stdin_open: true # equivalent to -it
7
+ tty: true # equivalent to -it
8
+ restart: 'no' # equivalent to --rm (don’t restart container automatically)
9
+ environment:
10
+ - PATH_RESULTS= /app/results
11
+ # - HF_ENDPOINT=http://172.16.15.118:9557/repository/atalink-hf-models
12
  volumes:
13
+ - atalink_data_cache:/home/user/.cache
14
+ - atalink_data_gfpgan:/home/user/app/gfpgan
15
+ # # - ./backup:/backup
16
+ deploy:
17
+ resources:
18
+ reservations:
19
+ devices:
20
+ - capabilities: [gpu]
21
  volumes:
22
+ atalink_data_cache:
23
  external: true
24
+ atalink_data_gfpgan:
25
  external: true
src/generate_batch.py CHANGED
@@ -77,8 +77,18 @@ def get_data(first_coeff_path, audio_path, device, ref_eyeblink_coeff_path, stil
77
  m = spec[seq, :]
78
  indiv_mels.append(m.T)
79
  indiv_mels = np.asarray(indiv_mels) # T 80 16
 
 
 
 
 
 
 
 
 
 
80
 
81
- ratio = generate_blink_seq_randomly(num_frames) # T
82
  source_semantics_path = first_coeff_path
83
  source_semantics_dict = scio.loadmat(source_semantics_path)
84
  ref_coeff = source_semantics_dict['coeff_3dmm'][:1,:70] #1 70
@@ -93,7 +103,8 @@ def get_data(first_coeff_path, audio_path, device, ref_eyeblink_coeff_path, stil
93
  div = num_frames//refeyeblink_num_frames
94
  re = num_frames%refeyeblink_num_frames
95
  refeyeblink_coeff_list = [refeyeblink_coeff for i in range(div)]
96
- refeyeblink_coeff_list.append(refeyeblink_coeff[:re, :64])
 
97
  refeyeblink_coeff = np.concatenate(refeyeblink_coeff_list, axis=0)
98
  print(refeyeblink_coeff.shape[0])
99
 
 
77
  m = spec[seq, :]
78
  indiv_mels.append(m.T)
79
  indiv_mels = np.asarray(indiv_mels) # T 80 16
80
+ if num_frames < 20:
81
+ print(f"[WARN] num_frames={num_frames} too small, enable still_mode / skip blink.")
82
+ still = True
83
+ use_blink = False
84
+
85
+ # Blink ratio
86
+ if use_blink and not still:
87
+ ratio = generate_blink_seq_randomly(num_frames) # T × 1
88
+ else:
89
+ ratio = np.zeros((num_frames, 1)) # không blink
90
 
91
+ # ratio = generate_blink_seq_randomly(num_frames) # T
92
  source_semantics_path = first_coeff_path
93
  source_semantics_dict = scio.loadmat(source_semantics_path)
94
  ref_coeff = source_semantics_dict['coeff_3dmm'][:1,:70] #1 70
 
103
  div = num_frames//refeyeblink_num_frames
104
  re = num_frames%refeyeblink_num_frames
105
  refeyeblink_coeff_list = [refeyeblink_coeff for i in range(div)]
106
+ if re > 0:
107
+ refeyeblink_coeff_list.append(refeyeblink_coeff[:re, :64])
108
  refeyeblink_coeff = np.concatenate(refeyeblink_coeff_list, axis=0)
109
  print(refeyeblink_coeff.shape[0])
110
 
utils/clear_results.sh ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ # Xóa toàn bộ dữ liệu trong thư mục /app/results vào 10h tối mỗi ngày
3
+ # Thêm dòng sau vào crontab khi build docker
4
+ # 0 22 * * * /app/clear_results.sh
5
+
6
+ rm -rf /app/results/*
7
+ echo "[CRON] Đã xóa dữ liệu trong /app/results lúc $(date)" >> /app/clear_results.log
8
+
9
+ rm -rf /app/tts_cache/*
10
+ echo "[CRON] Đã xóa dữ liệu trong /app/tts_cache lúc $(date)" >> /app/clear_tts_cache.log
utils/entrypoint.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+ # Đảm bảo /var/run tồn tại và user có quyền ghi
4
+ mkdir -p /var/run
5
+ chmod 777 /var/run
6
+ # Cài crontab với job xóa results
7
+ crontab -l 2>/dev/null | { cat; echo "0 22 * * * /home/user/app/utils/clear_results.sh"; } | crontab -
8
+ # Start cron background
9
+ cron
10
+ # Start app.py (foreground)
11
+ exec "$@"
utils/prepare_environment.py CHANGED
@@ -23,8 +23,8 @@ DOWNLOADS = [
23
  ]
24
 
25
 
26
- TMP_DIR = "/tmp"
27
- VOLUME_PREFIX = "test_"
28
 
29
 
30
  def download_file(url, dest_folder):
@@ -62,11 +62,11 @@ def create_volume_and_extract(tar_path, volume_name):
62
  "-v",
63
  f"{volume_name}:/data", # Mount Docker volume (volume_name) vào thư mục /data trong container
64
  "-v",
65
- f"{os.path.dirname(tar_path)}:/tmpdata", # Mount thư mục chứa file tar.gz trên host vào /tmpdata trong container
66
  "busybox", # Image dùng để chạy container (ở đây là Ubuntu 22.04)
67
- "bash",
68
  "-c", # Chạy lệnh bash trong container
69
- f"tar -xzf /tmpdata/{os.path.basename(tar_path)} -C /data", # Lệnh giải nén file tar.gz từ /tmpdata vào /data
70
  ],
71
  check=True,
72
  )
@@ -91,10 +91,19 @@ def main():
91
  VOLUME_PREFIX
92
  + os.path.splitext(os.path.splitext(os.path.basename(tar_path))[0])[0]
93
  )
94
- print(f"🚀 [VOLUME] Name: \033[1;33m{volume_name}\033[0m")
95
- # create_volume_and_extract(tar_path, volume_name)
96
- # cleanup_tmp(downloaded_files)
97
 
98
 
99
  if __name__ == "__main__":
100
  main()
 
 
 
 
 
 
 
 
 
 
23
  ]
24
 
25
 
26
+ TMP_DIR = "tmp"
27
+ VOLUME_PREFIX = "atalink_"
28
 
29
 
30
  def download_file(url, dest_folder):
 
62
  "-v",
63
  f"{volume_name}:/data", # Mount Docker volume (volume_name) vào thư mục /data trong container
64
  "-v",
65
+ f"{os.path.abspath(os.path.dirname(tar_path))}:/tmpdata", # Mount thư mục chứa file tar.gz trên host vào /tmpdata trong container
66
  "busybox", # Image dùng để chạy container (ở đây là Ubuntu 22.04)
67
+ "sh",
68
  "-c", # Chạy lệnh bash trong container
69
+ f"tar -xzvf /tmpdata/{os.path.basename(tar_path)} --strip 1 -C /data", # Lệnh giải nén file tar.gz từ /tmpdata vào /data
70
  ],
71
  check=True,
72
  )
 
91
  VOLUME_PREFIX
92
  + os.path.splitext(os.path.splitext(os.path.basename(tar_path))[0])[0]
93
  )
94
+ print(f"🚀 [VOLUME] Name: \033[1;33m{volume_name}\033[0m")
95
+ create_volume_and_extract(tar_path, volume_name)
96
+ cleanup_tmp(downloaded_files)
97
 
98
 
99
  if __name__ == "__main__":
100
  main()
101
+
102
+
103
+ # if __name__ == "__main__":
104
+ # Test create_volume_and_extract với file test_data_backup.tar.gz
105
+ # test_tar = os.path.join("tmp", "data_backup.tar.gz")
106
+ # if os.path.exists(test_tar):
107
+ # create_volume_and_extract(test_tar, "atalink_data_backup")
108
+ # else:
109
+ # print("File tmp/test_data_backup.tar.gz không tồn tại để test.")