Navid Arabi commited on
Commit
c8c252f
·
1 Parent(s): e1df50c

add gdrive file loader

Browse files
components/dashboard_page.py CHANGED
@@ -1,71 +1,119 @@
1
- # components/dashboard_page.py
 
 
2
  import gradio as gr
 
 
 
3
  from components.header import Header
 
 
 
 
 
 
4
 
5
 
6
  class DashboardPage:
7
- """UI elements + event wiring for the post-login dashboard."""
8
 
9
  # ───────── ساخت UI ───────── #
10
  def __init__(self) -> None:
11
  with gr.Column(visible=False) as self.container:
12
- # هدر بالا (نام کاربر + خروج)
13
  self.header = Header()
14
 
15
- # اطلاعات فایل صوتی و انوتیشن
16
  with gr.Row():
17
- with gr.Column():
18
- self.tts_id = gr.Textbox(
19
- label="tts_data.id", interactive=False
20
- )
21
- self.filename = gr.Textbox(
22
- label="tts_data.filename", interactive=False
23
- )
 
24
  self.sentence = gr.Textbox(
25
- label="tts_data.sentence", interactive=False
26
  )
 
27
  self.ann_sentence = gr.Textbox(
28
- label="annotations.annotated_sentence",
29
- interactive=False,
30
- )
31
- self.ann_at = gr.Textbox(
32
- label="annotations.annotated_at",
33
- interactive=False,
34
- )
35
- self.validated = gr.Checkbox(
36
- label="annotations.validated",
37
- interactive=False,
38
  )
39
 
40
- # دکمه‌های پیمایش
41
- with gr.Row():
42
- self.btn_prev = gr.Button("⬅️ Previous")
43
- self.btn_next = gr.Button("Next ➡️")
 
44
 
45
- # stateهای مخفی
46
- self.items_state = gr.State([]) # list[dict]
47
- self.idx_state = gr.State(0) # اندیس فعلی
 
 
 
 
 
 
48
 
 
 
 
 
49
 
 
 
 
50
 
51
- # ───────── wiring رویدادها ───────── #
52
  def register_callbacks(
53
  self,
54
- login_page, # برای اجازه‌ی logout در Header
55
- session_state: gr.State, # gr.State شامل دیکشنری نشست
56
- root_blocks: gr.Blocks # بلوک ریشه‌ی برنامه
57
  ) -> None:
58
 
59
- # ۱) رویداد خروج (در Header)
60
  self.header.register_callbacks(login_page, self, session_state)
61
 
62
- # ---------- توابع کمکی ---------- #
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def show_current(items: list, idx: int):
64
- """رکورد idx را روی فیلدها می‌ریزد؛ در صورت خالی بودن لیست مقادیر تهی."""
65
  if not items:
66
- return ["", "", "", "", "", False]
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  data = items[idx]
 
 
 
69
  return [
70
  data["id"],
71
  data["filename"],
@@ -73,6 +121,9 @@ class DashboardPage:
73
  data.get("annotated_sentence", ""),
74
  data.get("annotated_at", ""),
75
  bool(data.get("validated", False)),
 
 
 
76
  ]
77
 
78
  def next_idx(items: list, idx: int):
@@ -81,13 +132,13 @@ class DashboardPage:
81
  def prev_idx(items: list, idx: int):
82
  return max(idx - 1, 0)
83
 
84
- # ---------- بارگذاری اولیه (یک بار در شروع) ---------- #
85
  def load_items(sess: dict):
86
  items = sess.get("dashboard_items", [])
87
  return (
88
- items, # → items_state
89
- 0, # → idx_state
90
- *show_current(items, 0), # → شش فیلد
91
  )
92
 
93
  root_blocks.load(
@@ -102,49 +153,60 @@ class DashboardPage:
102
  self.ann_sentence,
103
  self.ann_at,
104
  self.validated,
 
 
 
105
  ],
106
  )
107
 
108
- # ---------- دکمه «قبلی» ----------
109
- (
110
- self.btn_prev
111
- .click(
112
- fn=prev_idx,
113
- inputs=[self.items_state, self.idx_state],
114
- outputs=self.idx_state,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  )
116
- .then(
117
- fn=show_current,
118
- inputs=[self.items_state, self.idx_state],
119
- outputs=[
120
- self.tts_id,
121
- self.filename,
122
- self.sentence,
123
- self.ann_sentence,
124
- self.ann_at,
125
- self.validated,
126
- ],
127
- )
128
- )
129
 
130
- # ---------- دکمه «بعدی» ----------
131
- (
132
- self.btn_next
133
- .click(
134
- fn=next_idx,
135
- inputs=[self.items_state, self.idx_state],
136
- outputs=self.idx_state,
137
- )
138
- .then(
139
- fn=show_current,
140
- inputs=[self.items_state, self.idx_state],
141
- outputs=[
142
- self.tts_id,
143
- self.filename,
144
- self.sentence,
145
- self.ann_sentence,
146
- self.ann_at,
147
- self.validated,
148
- ],
149
- )
150
- )
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+
4
  import gradio as gr
5
+ import numpy as np
6
+ from pydub import AudioSegment
7
+
8
  from components.header import Header
9
+ from utils.logger import Logger
10
+
11
+ log = Logger()
12
+
13
+ # اگر فایل‌های صوتی در پوشهٔ خاصی هستند این را عوض کنید
14
+ AUDIO_DIR = Path("audio") # <project_root>/audio/<filename>.wav
15
 
16
 
17
  class DashboardPage:
18
+ """صفحهٔ داشبورد شامل اطلاعات متنی (چپ) و پخش‌کنندهٔ صوت (راست)."""
19
 
20
  # ───────── ساخت UI ───────── #
21
  def __init__(self) -> None:
22
  with gr.Column(visible=False) as self.container:
23
+ # هدر
24
  self.header = Header()
25
 
26
+ # بدنهٔ دو ستونه
27
  with gr.Row():
28
+
29
+ # -------- ستونهٔ چپ : متادیتا -------- #
30
+ with gr.Column(scale=3) as self.left_col:
31
+
32
+ with gr.Row():
33
+ self.tts_id = gr.Textbox(label="ID", interactive=False)
34
+ self.filename = gr.Textbox(label="Filename", interactive=False)
35
+
36
  self.sentence = gr.Textbox(
37
+ label="Sentence", interactive=False, max_lines=5, rtl=True
38
  )
39
+
40
  self.ann_sentence = gr.Textbox(
41
+ label="Annotated Sentence",
42
+ interactive=True,
43
+ max_lines=5,
44
+ rtl=True,
 
 
 
 
 
 
45
  )
46
 
47
+ with gr.Row():
48
+ self.ann_at = gr.Textbox(
49
+ label="Annotation Time",
50
+ interactive=False,
51
+ )
52
 
53
+ self.validated = gr.Checkbox(
54
+ label="Annotation is Validate",
55
+ interactive=False,
56
+ )
57
+
58
+ # دکمه‌های پیمایش زیر اطلاعات متنی
59
+ with gr.Row():
60
+ self.btn_prev = gr.Button("⬅️ Previous")
61
+ self.btn_next = gr.Button("Next ➡️")
62
 
63
+ # -------- ستونهٔ راست : پخش‌کننده -------- #
64
+ with gr.Column(scale=2) as self.right_col:
65
+ self.audio = gr.Audio(label="🔊 Audio", interactive=False)
66
+
67
 
68
+ # stateهای مخفی
69
+ self.items_state = gr.State([]) # list[dict]
70
+ self.idx_state = gr.State(0) # اندیس فعلی
71
 
72
+ # ───────── wiring ───────── #
73
  def register_callbacks(
74
  self,
75
+ login_page,
76
+ session_state: gr.State, # dict درون gr.State
77
+ root_blocks: gr.Blocks,
78
  ) -> None:
79
 
80
+ # رویداد خروج
81
  self.header.register_callbacks(login_page, self, session_state)
82
 
83
+ # ---------- helpers ---------- #
84
+ def _audio_path(filename: str) -> str:
85
+ """مسیر کامل فایل صوتی روی دیسک."""
86
+ return str(AUDIO_DIR / filename)
87
+
88
+ def _duration_seconds(wav_path: str) -> float:
89
+ """طول فایل صوتی به ثانیه (برای اسلایدرها)."""
90
+ try:
91
+ dur = len(AudioSegment.from_file(wav_path)) / 1000.0
92
+ return round(dur, 2)
93
+ except Exception as e:
94
+ log.warning(f"Cannot read duration for '{wav_path}': {e}")
95
+ return 0.0
96
+
97
  def show_current(items: list, idx: int):
98
+ """داده‌های رکورد idx را برای خروجی‌ها تولید می‌کند."""
99
  if not items:
100
+ # 6 فیلد متنی + 3 فیلد صوت + validated
101
+ return [
102
+ "",
103
+ "",
104
+ "",
105
+ "",
106
+ "",
107
+ False,
108
+ None,
109
+ gr.update(minimum=0, maximum=0, value=0),
110
+ gr.update(minimum=0, maximum=0, value=0),
111
+ ]
112
 
113
  data = items[idx]
114
+ wav_path = _audio_path(data["filename"])
115
+ dur = _duration_seconds(wav_path)
116
+
117
  return [
118
  data["id"],
119
  data["filename"],
 
121
  data.get("annotated_sentence", ""),
122
  data.get("annotated_at", ""),
123
  bool(data.get("validated", False)),
124
+ wav_path, # audio
125
+ gr.update(minimum=0, maximum=dur, value=0), # start slider
126
+ gr.update(minimum=0, maximum=dur, value=dur), # end slider
127
  ]
128
 
129
  def next_idx(items: list, idx: int):
 
132
  def prev_idx(items: list, idx: int):
133
  return max(idx - 1, 0)
134
 
135
+ # ---------- initial load ---------- #
136
  def load_items(sess: dict):
137
  items = sess.get("dashboard_items", [])
138
  return (
139
+ items,
140
+ 0,
141
+ *show_current(items, 0),
142
  )
143
 
144
  root_blocks.load(
 
153
  self.ann_sentence,
154
  self.ann_at,
155
  self.validated,
156
+ self.audio,
157
+ self.start_slider,
158
+ self.end_slider,
159
  ],
160
  )
161
 
162
+ # ---------- prev / next buttons ---------- #
163
+ for btn, fn_nav in [(self.btn_prev, prev_idx), (self.btn_next, next_idx)]:
164
+ (
165
+ btn.click(
166
+ fn=fn_nav,
167
+ inputs=[self.items_state, self.idx_state],
168
+ outputs=self.idx_state,
169
+ ).then(
170
+ fn=show_current,
171
+ inputs=[self.items_state, self.idx_state],
172
+ outputs=[
173
+ self.tts_id,
174
+ self.filename,
175
+ self.sentence,
176
+ self.ann_sentence,
177
+ self.ann_at,
178
+ self.validated,
179
+ self.audio,
180
+ self.start_slider,
181
+ self.end_slider,
182
+ ],
183
+ )
184
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
+ # ---------- Play-Selection button ---------- #
187
+ def play_selection(wav_path: str, start: float, end: float):
188
+ """
189
+ بخش انتخاب‌شده از فایل را جدا می‌کند و به‌صورت
190
+ (sr, np.array) برمی‌گرداند تا در Player پخش شود.
191
+ """
192
+ if not wav_path or not os.path.exists(wav_path):
193
+ return None
194
+ try:
195
+ seg = AudioSegment.from_file(wav_path)
196
+ start_ms = int(max(start, 0) * 1000)
197
+ end_ms = int(min(end, len(seg) / 1000) * 1000)
198
+ if start_ms >= end_ms:
199
+ end_ms = start_ms + 1000 # حداقل ۱ ثانیه
200
+ clip = seg[start_ms:end_ms]
201
+ samples = np.array(clip.get_array_of_samples()).astype(np.float32)
202
+ samples /= np.iinfo(samples.dtype).max # نرمال‌سازی
203
+ return (clip.frame_rate, samples)
204
+ except Exception as e:
205
+ log.error(f"Cannot slice audio '{wav_path}': {e}")
206
+ return None
207
+
208
+ self.play_btn.click(
209
+ fn=play_selection,
210
+ inputs=[self.audio, self.start_slider, self.end_slider],
211
+ outputs=self.audio,
212
+ )
components/header.py CHANGED
@@ -14,11 +14,11 @@ class Header:
14
  def register_callbacks(self, login_page, dashboard_page, session_state):
15
  self.logout_btn.click(
16
  fn=AuthService.logout,
17
- inputs=session_state,
18
  outputs=[
19
- login_page.container,
20
- dashboard_page.container,
21
- self.welcome,
22
- login_page.message,
23
  ],
24
- )
 
14
  def register_callbacks(self, login_page, dashboard_page, session_state):
15
  self.logout_btn.click(
16
  fn=AuthService.logout,
17
+ inputs=[session_state], # ← حتماً داخل لیست
18
  outputs=[
19
+ login_page.container, # 1
20
+ dashboard_page.container, # 2
21
+ self.welcome, # 3
22
+ login_page.message, # 4
23
  ],
24
+ )
config.py CHANGED
@@ -12,6 +12,7 @@ class Config(BaseSettings):
12
  DB_NAME: str = os.getenv("DB_NAME", "defaultdb")
13
  HF_TOKEN: str = os.environ.get("HF_TOKEN")
14
  HF_TTS_DS_REPO: str = os.environ.get("HF_TTS_DS_REPO")
 
15
 
16
  APP_TITLE: str = "Gooya TTS Annotation Tools"
17
 
 
12
  DB_NAME: str = os.getenv("DB_NAME", "defaultdb")
13
  HF_TOKEN: str = os.environ.get("HF_TOKEN")
14
  HF_TTS_DS_REPO: str = os.environ.get("HF_TTS_DS_REPO")
15
+ GOOGLE_DRIVE_API_KEY: str = os.environ.get("GOOGLE_DRIVE_API_KEY")
16
 
17
  APP_TITLE: str = "Gooya TTS Annotation Tools"
18
 
gdrive_test.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from test.gdrive_downloader import PublicFolderAudioLoader
3
+ from config import conf
4
+
5
+ LOADER = PublicFolderAudioLoader(conf.GOOGLE_DRIVE_API_KEY)
6
+
7
+ def fetch_audio(folder_link, filename):
8
+ sr, wav = LOADER.load_audio(folder_link, filename)
9
+ return (sr, wav)
10
+
11
+ demo = gr.Interface(
12
+ fn=fetch_audio,
13
+ inputs=[
14
+ gr.Textbox(label="Folder URL or ID",
15
+ value="https://drive.google.com/drive/folders/15UllyqvOB8zmhzsTL8f1wmnK4OY2nzUQ?usp=sharing"),
16
+ gr.Textbox(label="Filename (e.g. 0001.wav)")
17
+ ],
18
+ outputs=gr.Audio(label="🔊 Audio"),
19
+ )
20
+
21
+ if __name__ == "__main__":
22
+ demo.launch()
requirements.txt CHANGED
@@ -6,4 +6,8 @@ soundfile
6
  librosa
7
  pydantic-settings
8
  pymysql
9
- bcrypt
 
 
 
 
 
6
  librosa
7
  pydantic-settings
8
  pymysql
9
+ bcrypt
10
+ google-api-python-client
11
+ pydub
12
+ numpy
13
+ requests
test/gdrive_downloader.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # gdrive_downloader.py
2
+
3
+ from __future__ import annotations
4
+ import io
5
+ import re
6
+ import numpy as np
7
+ from pydub import AudioSegment
8
+ from googleapiclient.discovery import build
9
+ from googleapiclient.http import MediaIoBaseDownload
10
+
11
+
12
+ def extract_folder_id(url_or_id: str) -> str:
13
+ """
14
+ اگر کاربر لینک فولدر بدهد ← ID را برمی‌گرداند.
15
+ اگر خودش ID باشد همان را برمی‌گرداند.
16
+ """
17
+ s = url_or_id.strip()
18
+ if "/" not in s and "?" not in s:
19
+ return s # احتمالاً خودش ID است
20
+ m = re.search(r"/folders/([a-zA-Z0-9_-]{10,})", s)
21
+ if not m:
22
+ raise ValueError("Cannot extract folder id from url")
23
+ return m.group(1)
24
+
25
+
26
+ class PublicFolderAudioLoader:
27
+ """
28
+ دانلودر فایل صوتی از فولدر عمومی گوگل‌درایو بدون ذخیره روی دیسک.
29
+
30
+ Parameters
31
+ ----------
32
+ api_key : str
33
+ Google API Key (کیِ عمومی؛ نه OAuth, نه سرویس‌اکانت).
34
+ """
35
+
36
+ def __init__(self, api_key: str) -> None:
37
+ self.svc = build("drive", "v3", developerKey=api_key, cache_discovery=False)
38
+
39
+ # ---------- helpers ---------- #
40
+ def _file_id_by_name(self, folder_id: str, filename: str) -> str:
41
+ q = (
42
+ f"'{folder_id}' in parents "
43
+ f"and name = '{filename}' "
44
+ f"and trashed = false"
45
+ )
46
+ rsp = (
47
+ self.svc.files()
48
+ .list(q=q, fields="files(id,name)", pageSize=5, supportsAllDrives=True)
49
+ .execute()
50
+ )
51
+ files = rsp.get("files", [])
52
+ if not files:
53
+ raise FileNotFoundError(f"'{filename}' not found in folder {folder_id}")
54
+ return files[0]["id"]
55
+
56
+ def _download_to_buf(self, file_id: str) -> io.BytesIO:
57
+ request = self.svc.files().get_media(fileId=file_id, supportsAllDrives=True)
58
+ buf = io.BytesIO()
59
+ downloader = MediaIoBaseDownload(buf, request)
60
+ done = False
61
+ while not done:
62
+ _, done = downloader.next_chunk()
63
+ buf.seek(0)
64
+ return buf
65
+
66
+ # ---------- public ---------- #
67
+ def load_audio(
68
+ self,
69
+ folder_url_or_id: str,
70
+ filename: str,
71
+ ) -> tuple[int, np.ndarray]:
72
+ # """
73
+ # فایل را به `(sample_rate, np.ndarray)` نرمال‌شده در بازه‌ی [-1,1] تبدیل می‌کند.
74
+ # """
75
+ folder_id = extract_folder_id(folder_url_or_id)
76
+ file_id = self._file_id_by_name(folder_id, filename)
77
+ buf = self._download_to_buf(file_id)
78
+ seg = AudioSegment.from_file(buf)
79
+ samples = np.array(seg.get_array_of_samples())
80
+
81
+ # اگر چندکاناله بود، شکل دهیم
82
+ if seg.channels > 1:
83
+ samples = samples.reshape(-1, seg.channels)
84
+
85
+ # ---------------------- نرمال‌سازی ----------------------
86
+ if np.issubdtype(samples.dtype, np.integer):
87
+ max_int = np.iinfo(samples.dtype).max # ← قبل از cast
88
+ samples = samples.astype(np.float32)
89
+ samples /= max_int # ← از max_int استفاده می‌کنیم
90
+ else:
91
+ # در حالت float
92
+ max_val = np.abs(samples).max()
93
+ if max_val > 1:
94
+ samples = samples / max_val
95
+ samples = samples.astype(np.float32)
96
+ # --------------------------------------------------------
97
+
98
+ return seg.frame_rate, samples
utils/auth.py CHANGED
@@ -137,16 +137,8 @@ class AuthService:
137
  session.clear()
138
  log.info(f"User '{username}' logged out.")
139
  return (
140
- gr.update(visible=True), # لاگین فرم را دوباره نشان بده
141
- gr.update(visible=False), # داشبورد را پنهان کن
142
- gr.update(value=""), # پیام‌ها را پاک کن
143
- gr.update(value=""), # متن خوش‌آمد را پاک کن
144
- [],
145
- 0,
146
- "",
147
- "",
148
- "",
149
- "",
150
- "",
151
- False, # خروجی‌های داشبورد را ریست
152
  )
 
137
  session.clear()
138
  log.info(f"User '{username}' logged out.")
139
  return (
140
+ gr.update(visible=True), # 1 → login_page.container
141
+ gr.update(visible=False), # 2 → dashboard_page.container
142
+ gr.update(value=""), # 3 → self.welcome
143
+ gr.update(value=""), # 4 → login_page.message
 
 
 
 
 
 
 
 
144
  )