main
  1#!/usr/bin/env python
  2# -*- coding: utf-8 -*-
  3import asyncio
  4import os
  5from pathlib import Path
  6
  7from cacheout import Cache
  8from cutword import Cutter
  9
 10# init some global instances
 11cache = Cache(ttl=0, maxsize=2048)
 12semaphore = asyncio.Semaphore(8)  # max 8 concurrent downloads
 13cutter = Cutter()
 14
 15DOWNLOAD_DIR = os.getenv("DOWNLOAD_DIR", Path(__file__).parent.joinpath("downloads").as_posix())
 16FONTS_DIR = os.getenv("FONTS_DIR", Path(DOWNLOAD_DIR).joinpath("fonts").as_posix())
 17FILE_SERVER = os.getenv("FILE_SERVER", "")  # expose the download dir to internet (optional). for example: https://server.com/dir
 18TZ = os.getenv("TZ", "Asia/Shanghai")
 19DEVICE_NAME = os.getenv("DEVICE_NAME", "BennyBot")
 20REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "60"))  # seconds
 21TEXT_LENGTH = int(os.getenv("TEXT_LENGTH", "4096"))  # Maximum length of text message
 22CAPTION_LENGTH = int(os.getenv("CAPTION_LENGTH", "1024"))  # 4096 for Premium user
 23MAX_FILE_BYTES = int(os.getenv("MAX_FILE_BYTES", "2000")) * 1024 * 1024  # 4000 MB for Premium user
 24MAX_MESSAGE_RETRIEVED = int(os.getenv("MAX_MESSAGE_RETRIEVED", "1000000"))  # Maximum number of messages to retrieve
 25MAX_MESSAGE_SUMMARY = int(os.getenv("MAX_MESSAGE_SUMMARY", "9999"))  # Maximum number of messages to summay
 26READING_SPEED = int(os.getenv("READING_SPEED", "600"))  # words per minute
 27DAILY_MESSAGES = os.getenv("DAILY_MESSAGES", "{}")  # Useful for daily checkin for some services. Should be a json string: '{"chat-1": "msg-1", "chat-2": "msg-2"}'
 28# For ytdlp downloaded video, re-encoding to H264 format. This set the max file size for re-encoding. Default: 1PB
 29YTDLP_RE_ENCODING_MAX_FILE_BYTES = int(os.getenv("YTDLP_RE_ENCODING_MAX_FILE_BYTES", "1125899906842624"))
 30# ytdlp max allowed file bytes. Default: 1PB (Set this if the VPS disk space is limited)
 31YTDLP_DOWNLOAD_MAX_FILE_BYTES = int(os.getenv("YTDLP_DOWNLOAD_MAX_FILE_BYTES", "1125899906842624"))
 32TELEGRAM_UA = os.getenv("TELEGRAM_UA", "TelegramBot (like TwitterBot)")
 33NUM_YOUTUBE_SEARCH_RESULTS = int(os.getenv("NUM_YOUTUBE_SEARCH_RESULTS", "10"))  # Number of youtube search results
 34NUM_GOOGLE_SEARCH_RESULTS = int(os.getenv("NUM_GOOGLE_SEARCH_RESULTS", "10"))  # Number of google search results
 35GOOGLE_SEARCH_GL = os.getenv("GOOGLE_SEARCH_GL", "cn")  # "gl" parameter (Geolocation)
 36CLEAN_OLD_FILES_OLDER_THAN_SECONDS = int(os.getenv("CLEAN_OLD_FILES_OLDER_THAN_SECONDS", "7200"))
 37ENABLE_CRONTAB = os.getenv("ENABLE_CRONTAB", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
 38
 39
 40class PREFIX:
 41    SOCIAL_MEDIA = os.getenv("PREFIX_SOCIAL_MEDIA", "/benny, /dl, !dl")
 42    AI_SUMMARY = os.getenv("PREFIX_AI_SUMMARY", "/summary").lower()
 43    AI_TEXT_GENERATION = os.getenv("PREFIX_AI_TEXT_GENERATION", "/ai").lower()
 44    AI_IMG_GENERATION = os.getenv("PREFIX_AI_IMG_GENERATION", "/img").lower()
 45    AI_VIDEO_GENERATION = os.getenv("PREFIX_AI_VIDEO_GENERATION", "/gvid").lower()
 46    ASR = os.getenv("PREFIX_ASR", "/asr").lower()
 47    AUDIO = os.getenv("PREFIX_AUDIO", "/audio").lower()
 48    CONVERT = os.getenv("PREFIX_CONVERT", "/convert").lower()  # convert image file to photo
 49    SUBTITLE = os.getenv("PREFIX_SUBTITLE", "/subtitle, /sub").lower()
 50    WGET = os.getenv("PREFIX_WGET", "/wget, /curl").lower()
 51    OCR = os.getenv("PREFIX_OCR", "/ocr").lower()
 52    PRICE = os.getenv("PREFIX_PRICE", "/price").lower()  # unify crypto, stock
 53    CRYPTO = os.getenv("PREFIX_CRYPTO", "/crypto").lower()  # crypto only
 54    STOCK = os.getenv("PREFIX_STOCK", "/stock").lower()  # stock only
 55    CHAT_SUMMARY = os.getenv("PREFIX_CHAT_SUMMARY", "/chatsum").lower()
 56    COMBINATION = os.getenv("PREFIX_COMBINATION", "/combine").lower()
 57    VOICE = os.getenv("PREFIX_VOICE", "/voice").lower()
 58    SEARCH_YOUTUBE = os.getenv("PREFIX_SEARCH_YOUTUBE", "/youtube, /ytb").lower()
 59    SEARCH_GOOGLE = os.getenv("PREFIX_SEARCH_GOOGLE", "/google").lower()
 60    DANMU = os.getenv("PREFIX_DANMU", "/danmu").lower()
 61    FAYAN = os.getenv("PREFIX_FAYAN", "/fa").lower()
 62    HISTORY = "/history, /hist"
 63    TTS = os.getenv("PREFIX_TTS", "/tts").lower()
 64    CONVERT_TO_TC = os.getenv("PREFIX_CONVERT_TO_TC", "/tc, /tw").lower()
 65    CONVERT_TO_SC = os.getenv("PREFIX_CONVERT_TO_SC", "/sc, /cn").lower()
 66    QUOTLY = os.getenv("PREFIX_QUOTLY", "/quote").lower()
 67    TMDB = os.getenv("PREFIX_TMDB", "/tmdb").lower()
 68    FFMPEG_CUT = os.getenv("PREFIX_FFMPEG_CUT", "/cut").lower()
 69    FFMPEG_H264 = os.getenv("PREFIX_FFMPEG_H264", "/h264").lower()
 70    FFPROBE = os.getenv("PREFIX_FFPROBE", "/ffprobe").lower()
 71    WATERMARK = os.getenv("PREFIX_WATERMARK", "/wm, /watermark").lower()
 72    VERSION = os.getenv("PREFIX_VERSION", "/version").lower()
 73    MSG_INFO = os.getenv("PREFIX_MSG_INFO", "/info").lower()
 74
 75
 76class API:
 77    FXTWITTER = os.getenv("FXTWITTER_API", "https://api.fxtwitter.com")
 78    DDINSTAGRAM = os.getenv("DDINSTAGRAM_API", "https://www.ddinstagram.com")
 79    TIKHUB = os.getenv("TIKHUB", "https://api.tikhub.io")
 80    TIKHUB_FREE = os.getenv("TIKHUB_FREE", "https://api.douyin.wtf")
 81    TIKHUB_INSTAGRAM = os.getenv("TIKHUB_INSTAGRAM_API", "https://api.tikhub.io/api/v1/instagram/v1/fetch_post_by_url?post_url=")
 82    TIKHUB_INSTAGRAM_STORY = os.getenv("TIKHUB_INSTAGRAM_STORY_API", "https://api.tikhub.io/api/v1/instagram/v3/get_user_stories?username=")
 83    TIKHUB_WEIBO_VIDEO = os.getenv("TIKHUB_WEIBO_VIDEO_API", "https://api.tikhub.io/api/v1/weibo/web/fetch_short_video_data?share_text=")
 84    TIKHUB_WECHAT = os.getenv("TIKHUB_WECHAT", "https://api.tikhub.io/api/v1/wechat_mp/web/fetch_mp_article_detail_json?url=")
 85    BINANCE_SPOT = os.getenv("BINANCE_SPOT_API", "https://data-api.binance.vision")
 86    BINANCE_UM = os.getenv("BINANCE_UM_API", "https://fapi.binance.com")
 87    OKX = os.getenv("OKX_API", "https://www.okx.com")
 88    QUOTELY = os.getenv("QUOTLY_API", "https://bot.lyo.su/quote/generate")
 89
 90
 91class DANMU:
 92    BASE_URL = os.getenv("DANMU_BASE_URL", "")  # Custom API, No docs
 93    STREAMER = os.getenv("DANMU_STREAMER", "Streamer")  # streamer name
 94    AUTH_USER = os.getenv("DANMU_AUTH_USER", "")  # username for basic auth
 95    AUTH_PASS = os.getenv("DANMU_AUTH_PASS", "")  # password for basic auth
 96    QUERY_METHOD = os.getenv("DANMU_QUERY_METHOD", "turso")  # Turso or R2+API server
 97    NUM_PER_QUERY = int(os.getenv("DANMU_NUM_PER_QUERY", "100"))  # Number of items per query to API server
 98    D1_DATABASE = os.getenv("DANMU_D1_DATABASE", "bennybot-danmu")
 99    TURSO_DATABASE = os.getenv("DANMU_TURSO_DATABASE", "bennybot-danmu")
100    TURSO_USERNAME = os.getenv("DANMU_TURSO_USERNAME", "")  # https://turso.tech
101    TURSO_API_TOKEN = os.getenv("DANMU_TURSO_API_TOKEN", "")
102    TURSO_GROUP_TOKEN = os.getenv("DANMU_TURSO_GROUP_TOKEN", "")
103    R2_PREFIX = os.getenv("DANMU_R2_PREFIX", "Streaming/")
104    SYNC_ENABLE = os.getenv("DANMU_SYNC_ENABLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
105    SYNC_ENGNIE = os.getenv("DANMU_SYNC_ENGNIE", "turso,R2")  # sync livechats to Turso & R2
106    SYNC_DANMU_YEARS = os.getenv("SYNC_DANMU_YEARS", "")  # comma separated years to sync. e.g. "2025,2024,2023"
107    SYNC_FAYAN_YEARS = os.getenv("SYNC_FAYAN_YEARS", "")  # comma separated years that has live stream
108
109
110class PROVIDER:  # default API provider
111    DOUYIN = os.getenv("DOUYIN_PROVIDER", "direct-free-tikhub-bridge").lower()
112    DOUYIN_COMMENTS = os.getenv("DOUYIN_COMMENTS_PROVIDER", "free-tikhub").lower()  # a false value (0, false, none, null) to disable it
113    INSTAGRAM = os.getenv("INSTAGRAM_PROVIDER", "tikhub-ddinstagram-bridge").lower()
114    WEIBO = os.getenv("WEIBO_PROVIDER", "direct-bridge").lower()
115    XHS = os.getenv("XHS_PROVIDER", "direct-bridge").lower()
116
117
118class TOKEN:
119    SESSION_STRING = os.getenv("SESSION_STRING", "")
120    TIKHUB = os.getenv("TIKHUB_TOKEN", "")
121    YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY", "")
122    CMC_API_KEY = os.getenv("CMC_API_KEY", "")
123    GOOGLE_SEARCH_API_KEY = os.getenv("GOOGLE_SEARCH_API_KEY", "")
124    GOOGLE_SEARCH_CX = os.getenv("GOOGLE_SEARCH_CX", "")
125    CHART_IMG = os.getenv("CHART_IMG_KEY", "")
126    TELEGRAPH = os.getenv("TELEGRAPH_TOKEN", "")
127    NEOCITIES = os.getenv("NEOCITIES_USERPASS", "")  # in "user,pass" format
128    NEOCITIES_IV_HASH = os.getenv("NEOCITIES_INSTANTVIEW_HASH", "")
129    R2_IV_HASH = os.getenv("R2_INSTANTVIEW_HASH", "")
130    GITHUB = os.getenv("GITHUB_TOKEN", "")
131    SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID", "")
132    SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET", "")
133    V2EX = os.getenv("V2EX_TOKEN", "")
134    TMDB = os.getenv("TMDB_TOKEN", "")
135    SPOOME = os.getenv("SPOOME_TOKEN", "")
136
137
138class PROXY:  # format: socks5://127.0.0.1:7890
139    AI_POST = os.getenv("AI_POST_PROXY", None)
140    ALI = os.getenv("ALI_PROXY", None)
141    ANTHROPIC = os.getenv("ANTHROPIC_PROXY", None)
142    CLOUDFLARE = os.getenv("CLOUDFLARE_PROXY", None)
143    COOKIE_CLOUD = os.getenv("COOKIE_CLOUD_PROXY", None)
144    CRYPTO = os.getenv("CRYPTO_PROXY", None)
145    D1 = os.getenv("D1_PROXY", None)
146    DANMU = os.getenv("DANMU_PROXY", None)
147    DOUYIN = os.getenv("DOUYIN_PROXY", None)
148    DOWNLOAD = os.getenv("DOWNLOAD_PROXY", None)
149    EDGE = os.getenv("TTS_EDGE_PROXY", None)
150    GITHUB = os.getenv("GITHUB_PROXY", None)
151    GOOGLE = os.getenv("GOOGLE_PROXY", None)
152    GROQ = os.getenv("GROQ_PROXY", None)  # Ban CN & HK IP
153    IMG = os.getenv("IMG_PROXY", "")  # https://caravaggio.ramielcreations.com/docs/install
154    INSTAGRAM = os.getenv("INSTAGRAM_PROXY", None)
155    OPENAI = os.getenv("OPENAI_PROXY", None)
156    PODCAST = os.getenv("PODCAST_PROXY", None)
157    REDDIT = os.getenv("REDDIT_PROXY", None)
158    SPOTIFY = os.getenv("SPOTIFY_PROXY", None)
159    SUBTITLE = os.getenv("SUBTITLE_PROXY", None)
160    TELEGRAM = os.getenv("TELEGRAM_PROXY", None)  # Telegram
161    TENCENT = os.getenv("TENCENT_PROXY", None)  # Banned oversea IP, need a back to China proxy
162    TIKTOK = os.getenv("TIKTOK_PROXY", None)
163    TMDB = os.getenv("TMDB_PROXY", None)
164    TURSO = os.getenv("TURSO_PROXY", None)
165    TWITTER = os.getenv("TWITTER_PROXY", None)
166    V2EX = os.getenv("V2EX_PROXY", None)
167    WARP = os.getenv("WARP_PROXY", None)
168    WECHAT = os.getenv("WECHAT_PROXY", None)
169    WEIBO = os.getenv("WEIBO_PROXY", None)
170    ARXIV = os.getenv("ARXIV_PROXY", None)
171    XHS = os.getenv("XHS_PROXY", None)  # Banned VPS IP, need residential proxy
172    YTDLP = os.getenv("YTDLP_PROXY", None)  # general proxy for ytdlp
173    YTDLP_FALLBACK = os.getenv("YTDLP_PROXY_FALLBACK", None)  # fallback proxy for ytdlp
174    # for ytdlp proxy of specific sites (Like Bilibili), use this format: YTDLP_PROXY_BILIBILI
175
176
177class COOKIE:  # See: https://github.com/easychen/CookieCloud
178    CLOUD_SERVER = os.getenv("COOKIE_CLOUD_SERVER", "")
179    CLOUD_KEY = os.getenv("COOKIE_CLOUD_KEY", "")
180    CLOUD_PASS = os.getenv("COOKIE_CLOUD_PASS", "")
181
182
183class TID:  # see more TID usecase in `src/permission.py`
184    ME = int(os.getenv("TID_ME", "0"))  # a temperary chat for some tasks
185    ADMIN = os.getenv("TID_ADMIN", "")  # comma separated userid or @username
186    TEMP = os.getenv("TID_TEMP", "me")  # a temperary chat for some tasks
187    HISTORY_ADMIN = os.getenv("TID_HISTORY_ADMIN", "")  # comma separated userid (@username is NOT supported!)
188    # back up ytdlp audio if the user does not request it
189    DAILY_SUMMARY = os.getenv("TID_DAILY_SUMMARY", "{}")  # {"source-chat-id": "target-chat-id"}, e.g. '{"-1001234567890": "-1009876543210"}'
190    GEMINI_CHATS = os.getenv("TID_GEMINI_CHATS", "")  # comma separated chat ids to always use gemini models (no need `/gemini`)
191    OPENAI_CHATS = os.getenv("TID_OPENAI_CHATS", "")  # comma separated chat ids to always use openai models (no need `/gpt`)
192    DEEPSEEK_CHATS = os.getenv("TID_DEEPSEEK_CHATS", "")  # comma separated chat ids to always use deepseek models (no need `/ds`)
193    QWEN_CHATS = os.getenv("TID_QWEN_CHATS", "")  # comma separated chat ids to always use qwen models (no need `/qwen`)
194    DOUBAO_CHATS = os.getenv("TID_DOUBAO_CHATS", "")  # comma separated chat ids to always use doubao models (no need `/doubao`)
195    GROK_CHATS = os.getenv("TID_GROK_CHATS", "")  # comma separated chat ids to always use grok models (no need `/grok`)
196    KIMI_CHATS = os.getenv("TID_KIMI_CHATS", "")  # comma separated chat ids to always use kimi models (no need `/kimi`)
197
198
199class DB:
200    CF_KV_ENABLED = os.getenv("CF_KV_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
201    CF_ACCOUNT_ID = os.getenv("CF_ACCOUNT_ID", "")
202    CF_API_TOKEN = os.getenv("CF_API_TOKEN", "")
203    CF_KV_NAMESPACE_ID = os.getenv("CF_KV_NAMESPACE_ID", "")
204    CF_R2_ENABLED = os.getenv("CF_R2_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
205    CF_R2_BUCKET_NAME = os.getenv("CF_R2_BUCKET_NAME", "bennybot")
206    CF_R2_ACCESS_KEY_ID = os.getenv("CF_R2_ACCESS_KEY_ID", "")
207    CF_R2_SECRET_ACCESS_KEY = os.getenv("CF_R2_SECRET_ACCESS_KEY", "")
208    CF_R2_PUBLIC_URL = os.getenv("CF_R2_PUBLIC_URL", "")
209    CF_D1_ENABLED = os.getenv("CF_D1_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
210    ALIST_ENABLED = os.getenv("ALIST_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
211    ALIST_USERNAME = os.getenv("ALIST_USERNAME", "guest")
212    ALIST_PASSWORD = os.getenv("ALIST_PASSWORD", "guest")
213    ALIST_SERVER = os.getenv("ALIST_SERVER", "")
214    ALIST_BASR_PATH = os.getenv("ALIST_BASR_PATH", "")
215    PASTBIN_SERVER = os.getenv("PASTBIN_SERVER", "https://shz.al")
216    PASTBIN_MAX_BYTES = int(os.getenv("PASTBIN_MAX_BYTES", "10485760"))  # 10 MB
217    TURSO_ENABLED = os.getenv("TURSO_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
218    TURSO_USERNAME = os.getenv("TURSO_USERNAME", "")  # https://turso.tech
219    TURSO_API_TOKEN = os.getenv("TURSO_API_TOKEN", "")
220    TURSO_GROUP_TOKEN = os.getenv("TURSO_GROUP_TOKEN", "")
221    GH_USER = os.getenv("DB_GH_USER", "")
222    GH_REPO = os.getenv("DB_GH_REPO", "bennybot")  # just repo name, not `owner/repo`
223    GH_TOKEN = os.getenv("DB_GH_TOKEN", "")
224
225
226class HISTORY:
227    ENABLE = os.getenv("HISTORY_ENABLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
228    ENGINE = os.getenv("HISTORY_ENGINE", "turso").lower()  # turso or d1 (This is for sync & backup)
229    QUERY_ENGINE = os.getenv("HISTORY_QUERY_ENGINE", "turso").lower()  # turso or d1
230    TURSO_ENABLE = os.getenv("HISTORY_TURSO_ENABLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
231    TURSO_DATABASE = os.getenv("HISTORY_TURSO_DATABASE", "bennybot-history")
232    TURSO_USERNAME = os.getenv("HISTORY_TURSO_USERNAME", "")  # https://turso.tech
233    TURSO_API_TOKEN = os.getenv("HISTORY_TURSO_API_TOKEN", "")
234    TURSO_GROUP_TOKEN = os.getenv("HISTORY_TURSO_GROUP_TOKEN", "")
235    PERIODICALLY_BACKUP_CHATS = os.getenv("HISTORY_PERIODICALLY_BACKUP_CHATS", "")  # "full_table" or comma separated chat ids to include  (without `-100` prefix)
236    BACKUP_CHATS_HOURS = float(os.getenv("HISTORY_BACKUP_CHATS_HOURS", "24"))  # hours to backup chats
237    D1_ENABLE = os.getenv("HISTORY_D1_ENABLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
238    D1_DATABASE = os.getenv("HISTORY_D1_DATABASE", "bennybot-history")
239    INCLUDE_CHATS = os.getenv("HISTORY_INCLUDE_CHATS", "")  # "all" or comma separated chat ids to include  (without `-100` prefix)
240    IGNORE_CHATS = os.getenv("HISTORY_IGNORE_CHATS", "")  # comma separated chat ids to ignore  (without `-100` prefix)
241    INCLUDE_PRIVATES = os.getenv("HISTORY_INCLUDE_PRIVATES", "")  # "all" or comma separated private chat ids to include (without `-100` prefix)
242    INCLUDE_BOTS = os.getenv("HISTORY_INCLUDE_BOTS", "")  # "all" or comma separated private chat ids to include  (without `-100` prefix)
243    INCLUDE_GROUPS = os.getenv("HISTORY_INCLUDE_GROUPS", "")  # "all" or comma separated private chat ids to include  (without `-100` prefix)
244    INCLUDE_CHANNELS = os.getenv("HISTORY_INCLUDE_CHANNELS", "")  # "all" or comma separated private chat ids to include  (without `-100` prefix)
245
246
247class ASR:
248    # use different engines based on duration
249    # support ali, tencent, gemini, deepgram, cloudflare, groq
250    DEFAULT_ENGINE = os.getenv("ASR_DEFAULT_ENGINE", "auto")
251    SHORT_ENGINE = os.getenv("ASR_SHORT_ENGINE", "tencent")  # comma separated engine names
252    SHORT_DURATION = int(os.getenv("ASR_SHORT_DURATION", "60"))
253    MIDDLE_ENGINE = os.getenv("ASR_MIDDLE_ENGINE", "tencent,ali")  # comma separated engine names
254    MIDDLE_DURATION = int(os.getenv("ASR_MIDDLE_DURATION", "600"))
255    LONG_ENGINE = os.getenv("ASR_LONG_ENGINE", "gemini")  # comma separated engine names
256
257    TENCENT_APPID = os.getenv("ASR_TENCENT_APPID", "")
258    TENCENT_SECRET_ID = os.getenv("ASR_TENCENT_SECRET_ID", "")
259    TENCENT_SECRET_KEY = os.getenv("ASR_TENCENT_SECRET_KEY", "")
260    TENCENT_FS_ENGINE = os.getenv("ASR_TENCENT_FS_ENGINE", "local")  # local, uguu or alist.
261    # WARN: some models do not allow oversea VPS. Can upload to an alist server in China.
262    ALI_MODEL = os.getenv("ASR_ALI_MODEL", "paraformer-realtime-v2,paraformer-realtime-v1")  # comma separated keys for load balance. e.g. "model1,model2,model3"
263    ALI_API_KEY = os.getenv("ASR_ALI_API_KEY", "")  # comma separated keys for load balance. e.g. "key1,key2,key3"
264    # If the bot is running on an oversea VPS, and Ali ASR model doesn't allow oversea fileserver.
265    # Change ASR_ALI_FS_ENGINE to alist (configurations in DB class)
266    ALI_FS_ENGINE = os.getenv("ASR_ALI_FS_ENGINE", "local")  # local, uguu or alist.
267    DEEPGRAM_API = os.getenv("ASR_DEEPGRAM_API", "")  # comma separated keys for load balance. e.g. "key1,key2,key3"
268    CLOUDFLARE_MODEL = os.getenv("ASR_CLOUDFLARE_MODEL", "@cf/openai/whisper-large-v3-turbo")
269    CLOUDFLARE_CHUNK_SECONDS = float(os.getenv("ASR_CLOUDFLARE_CHUNK_SECONDS", "180"))  # split long audio file into chunks
270    CLOUDFLARE_OVERLAP_SECONDS = float(os.getenv("ASR_CLOUDFLARE_OVERLAP_SECONDS", "5"))  # overlap seconds between chunks
271    CLOUDFLARE_KEYS = os.getenv("ASR_CLOUDFLARE_KEYS", "")  # comma separated keys for load balance. e.g. "AccountID:API_TOKEN, AccountID:API_TOKEN, ..."
272
273    GROQ_MAX_BYTES = int(os.getenv("ASR_GROQ_MAX_BYTES", "26214400"))  # 25MB (max file bytes for single file)
274    GROQ_CHUNK_SECONDS = float(os.getenv("ASR_GROQ_CHUNK_SECONDS", "180"))  # split long audio file into chunks
275    GROQ_OVERLAP_SECONDS = float(os.getenv("ASR_GROQ_OVERLAP_SECONDS", "5"))  # overlap seconds between chunks
276    GROQ_KEYS = os.getenv("ASR_GROQ_KEYS", "")  # comma separated keys for load balance.
277    GROQ_MODELS = os.getenv("ASR_GROQ_MODELS", "whisper-large-v3")  # comma separated model names.
278
279    GEMINI_CHUNK_SECONDS = float(os.getenv("ASR_GEMINI_CHUNK_SECONDS", "600"))  # split long audio file into chunks
280    GEMINI_OVERLAP_SECONDS = float(os.getenv("ASR_GEMINI_OVERLAP_SECONDS", "5"))  # overlap seconds between chunks
281    GEMINI_MAX_DURATION = int(os.getenv("ASR_GEMINI_MAX_DURATION", "34200"))  # 9.5 hour
282    GEMINI_MODEL = os.getenv("ASR_GEMINI_MODEL", "gemini-2.5-flash")
283    GEMINI_CONFIG = os.getenv("ASR_GEMINI_CONFIG", "{}")  # default config passed to GenerateContentConfig. Should be a json string: '{"key": "value"}'
284
285
286class PODCAST:
287    FEED_URLS = os.getenv("PODCAST_FEED_URLS", "")  # comma separated feed urls
288    OPML_URLS = os.getenv("PODCAST_OPML_URLS", "")  # comma separated opml urls
289    YOUTUBE_CHANNEL_IDS = os.getenv("PODCAST_YOUTUBE_CHANNEL_IDS", "")  # comma separated youtube channel ids
290    TID = int(os.getenv("PODCAST_TID", "0"))  # send to this chat id
291    FS_ENGINE = os.getenv("PODCAST_FS_ENGINE", "CF-R2")  # file storage engine for hosting podcast feeds
292    ASR_ENGINE = os.getenv("PODCAST_ASR_ENGINE", "auto")  # default ASR engine
293    IGNORE_OLD_THAN_SECONDS = int(os.getenv("PODCAST_IGNORE_OLD_THAN_SECONDS", "14400"))  # in seconds
294    KEEP_LATEST_ENTRIES = int(os.getenv("PODCAST_KEEP_LATEST_ENTRIES", "99999999"))  # keep latest entries
295    # To bypass censorship, set asr engines here (comma separated titles or domains)
296    ASR_FORCE_GEMINI_TITLES = os.getenv("PODCAST_ASR_FORCE_GEMINI_TITLES", "")
297    ASR_FORCE_GEMINI_DOMAINS = os.getenv("PODCAST_ASR_FORCE_GEMINI_DOMAINS", "")
298    ASR_FORCE_GROQ_TITLES = os.getenv("PODCAST_ASR_FORCE_GROQ_TITLES", "")
299    ASR_FORCE_GROQ_DOMAINS = os.getenv("PODCAST_ASR_FORCE_GROQ_DOMAINS", "")
300    ASR_FORCE_CLOUDFLARE_TITLES = os.getenv("PODCAST_ASR_FORCE_CLOUDFLARE_TITLES", "")
301    ASR_FORCE_CLOUDFLARE_DOMAINS = os.getenv("PODCAST_ASR_FORCE_CLOUDFLARE_DOMAINS", "")
302    ASR_FORCE_WHISPER_TITLES = os.getenv("PODCAST_ASR_FORCE_WHISPER_TITLES", "")
303    ASR_FORCE_WHISPER_DOMAINS = os.getenv("PODCAST_ASR_FORCE_WHISPER_DOMAINS", "")
304    ASR_FORCE_UNCENSORED_TITLES = os.getenv("PODCAST_ASR_FORCE_UNCENSORED_TITLES", "")
305    ASR_FORCE_UNCENSORED_DOMAINS = os.getenv("PODCAST_ASR_FORCE_UNCENSORED_DOMAINS", "anchor.fm,feeds.acast.com")
306    GH_REPO = os.getenv("PODCAST_GH_REPO", "podcast")
307    GH_TOKEN = os.getenv("PODCAST_GH_TOKEN", "")
308
309
310class FAVORITE:
311    ENABLE_SEND = os.getenv("ENABLE_FAVORITE_SEND", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
312    ENABLE_SAVE = os.getenv("ENABLE_FAVORITE_SAVE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
313    SEND_PREFIX = os.getenv("FAVORITE_SEND_PREFIX", "/fav").lower()
314    SAVE_PREFIX = os.getenv("FAVORITE_SAVE_PREFIX", "/save").lower()
315    R2_PREFIX = os.getenv("FAVORITE_R2_PREFIX", "Favorite/")
316    BACKUP_CHAT = os.getenv("FAVORITE_BACKUP_CHAT", "")  # chat id to backup favorite messages
317    TIDS_ALLOW_SEND = os.getenv("FAVORITE_TIDS_ALLOW_SEND", "all").lower()  # or comma separated telegram uids
318    TIDS_ALLOW_SAVE = os.getenv("FAVORITE_TIDS_ALLOW_SAVE", "")  # comma separated telegram uids
319
320
321class TTS:
322    # TTS related
323    DEFAULT_ENGINE = os.getenv("TTS_DEFAULT_ENGINE", "edge")  # edge, gemini, qwen, sambert
324    GEMINI_MODEL = os.getenv("TTS_GEMINI_MODEL", "gemini-2.5-flash-preview-tts")
325    GEMINI_INPUT_TOKEN_LIMIT = int(os.getenv("TTS_GEMINI_INPUT_TOKEN_LIMIT", "8192"))  # token limit of the tts model
326    GEMINI_SPLIT_LENGTH = int(os.getenv("TTS_GEMINI_SPLIT_LENGTH", "8192"))  # split token limit of the tts model
327    GEMINI_VOICE = os.getenv("TTS_GEMINI_VOICE", "Sulafat")
328    ALI_API_KEY = os.getenv("TTS_ALI_API_KEY", "")  # comma separated keys for load balance. e.g. "key1,key2,key3"
329    QWEN_MODEL = os.getenv("TTS_QWEN_MODEL", "qwen-tts,qwen-tts-latest")  # comma separated keys for load balance.
330    QWEN_INPUT_TOKEN_LIMIT = int(os.getenv("TTS_QWEN_INPUT_TOKEN_LIMIT", "512"))  # token limit of the tts model
331    QWEN_SPLIT_LENGTH = int(os.getenv("TTS_QWEN_SPLIT_LENGTH", "512"))  # split token limit of the tts model
332    QWEN_VOICE = os.getenv("TTS_QWEN_VOICE", "Chelsie")
333    SAMBERT_MODEL = os.getenv("TTS_SAMBERT_MODEL", "ramdom")  # comma separated models for load balance. use "random" to randomly choose a model
334    SAMBERT_LENGTH_LIMIT = int(os.getenv("TTS_SAMBERT_LENGTH_LIMIT", "20000"))  # token limit of the tts model
335    EDGE_DOMAIN = os.getenv("TTS_EDGE_DOMAIN", "https://tts.wangwangit.com")
336    EDGE_VOICE = os.getenv("TTS_EDGE_VOICE", "晓晓")
337    EDGE_MODEL = os.getenv("TTS_EDGE_MODEL", "zh-CN-XiaoxiaoNeural")
338
339
340class AI:
341    # Text Generation
342    MAX_CONTEXTS_NUM = int(os.getenv("AI_MAX_CONTEXTS_NUM", "30"))
343    TEXT_MODEL_CONFIG_KEY = os.getenv("AI_MODEL_CONFIG_KEY", "AI-TEXT")  # model configuration key in CF-KV
344    TEXT_GENERATION_DEFAULT_MODEL = os.getenv("AI_TEXT_GENERATION_DEFAULT_MODEL", "gemini")
345    GEMINI_MODEL_ID = os.getenv("AI_GEMINI_MODEL_ID", "gemini-2.5-flash")
346    GEMINI_API_KEYS = os.getenv("AI_GEMINI_API_KEYS", "")  # comma separated keys for load balance. e.g. "key1,key2,key3"
347    GEMINI_BASE_URL = os.getenv("AI_GEMINI_BASE_URL", "https://generativelanguage.googleapis.com")
348    GEMINI_DEFAULT_HEADERS = os.getenv("AI_GEMINI_DEFAULT_HEADERS", "{}")  # default headers passed to Gemini API. Should be a json string: '{"key": "value"}'
349    GEMINI_FILES_TTL = int(os.getenv("AI_GEMINI_FILES_TTL", "172800"))  # clean gemini files after 48 hours
350
351    ANTHROPIC_MODEL_ID = os.getenv("AI_ANTHROPIC_MODEL_ID", "claude-opus-4-6")
352    ANTHROPIC_API_KEYS = os.getenv("AI_ANTHROPIC_API_KEYS", "")  # comma separated keys for load balance. e.g. "key1,key2,key3"
353    ANTHROPIC_BASE_URL = os.getenv("AI_ANTHROPIC_BASE_URL", "https://api.anthropic.com")
354    ANTHROPIC_DEFAULT_HEADERS = os.getenv("AI_ANTHROPIC_DEFAULT_HEADERS", "{}")  # default headers passed to Anthropic API. Should be a json string: '{"key": "value"}'
355    ANTHROPIC_FILES_TTL = int(os.getenv("AI_ANTHROPIC_FILES_TTL", "172800"))  # clean anthropic files after 48 hours
356
357    OPENAI_MODEL_ID = os.getenv("AI_OPENAI_MODEL_ID", "gpt-4o")
358    OPENAI_API_KEYS = os.getenv("AI_OPENAI_API_KEYS", "")  # comma separated keys for load balance. e.g. "key1,key2,key3"
359    OPENAI_BASE_URL = os.getenv("AI_OPENAI_BASE_URL", "https://api.openai.com/v1")
360    TOOL_CALL_MODEL_ALIAS = os.getenv("AI_TOOL_CALL_MODEL_ALIAS", "tool-call")
361    AI_SUMMARY_MODEL_ALIAS = os.getenv("AI_SUMMARY_MODEL_ALIAS", "gemini")
362    PODCAST_SUMMARY_MODEL_ALIAS = os.getenv("PODCAST_SUMMARY_MODEL_ALIAS", "podcast-summary")
363    SUBTITLE_SUMMARY_MODEL_ALIAS = os.getenv("SUBTITLE_SUMMARY_MODEL_ALIAS", "subtitle-summary")
364    CHAT_SUMMARY_MODEL_ALIAS = os.getenv("CHAT_SUMMARY_MODEL_ALIAS", "chat-summary")
365
366    # Image Generation
367    IMG_MODEL_CONFIG_KEY = os.getenv("AI_IMG_MODEL_CONFIG_KEY", "AI-IMG")  # model configuration key in CF-KV
368    IMG_GENERATION_DEFAULT_MODEL = os.getenv("AI_IMG_GENERATION_DEFAULT_MODEL", "gpt")
369
370    # Video Generation
371    VIDEO_MODEL_CONFIG_KEY = os.getenv("AI_VIDEO_MODEL_CONFIG_KEY", "AI-VIDEO")  # model configuration key in CF-KV
372    VIDEO_GENERATION_DEFAULT_MODEL = os.getenv("AI_VIDEO_GENERATION_DEFAULT_MODEL", "seedance")