Tooling for managing asset compression, storage, and retrieval
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

310 lines
11 KiB

  1. import importlib.metadata
  2. import json
  3. import logging
  4. import typing
  5. from pydantic import BaseModel, BaseSettings, validator
  6. from acm.utility import get_string_sha256sum, get_string_xor
  7. from acm.version import VERSION
  8. LOG = logging.getLogger("acm.config")
  9. class ACMProfileProcessorOptions(BaseModel):
  10. force_preserve_smaller_input: bool = False
  11. class ACMProfileProcessor(BaseModel):
  12. name: str
  13. version: typing.Optional[str]
  14. processors: typing.List[str]
  15. extensions: typing.List[str]
  16. output_extension: str
  17. options: ACMProfileProcessorOptions
  18. command: str
  19. signature: typing.Optional[str]
  20. @validator('version', always=True)
  21. def version_validator(cls, v, values) -> str:
  22. # TODO Set the version to the app version if not provided
  23. if v is None:
  24. return VERSION
  25. @validator('signature', always=True)
  26. def signature_validator(cls, v, values) -> str:
  27. signature_keys = ["name", "version", "processors", "extensions", "output_extension", "command"]
  28. signature_values = [value for key, value in values.items() if key in signature_keys]
  29. return get_string_sha256sum(json.dumps(signature_values))
  30. class ACMProfile(BaseModel):
  31. name: str
  32. version: typing.Optional[str]
  33. processors: typing.List[ACMProfileProcessor]
  34. signature: typing.Optional[str]
  35. @validator('version', always=True)
  36. def version_validator(cls, v, values) -> str:
  37. if v is None:
  38. return VERSION
  39. @validator('signature', always=True)
  40. def hash_signature_validator(cls, v, values) -> str:
  41. signature_keys = ["name", "version"]
  42. signature_values = [value for key, value in values.items() if key in signature_keys]
  43. signature = get_string_sha256sum(json.dumps(signature_values))
  44. processor_signatures = [processor.signature for processor in values["processors"]]
  45. if len(processor_signatures) > 1:
  46. combined_processor_signature = get_string_xor(*processor_signatures)
  47. else:
  48. combined_processor_signature = processor_signatures[0]
  49. return get_string_sha256sum(signature + combined_processor_signature)
  50. def get_processor_names(self) -> typing.List[str]:
  51. return [processor.name for processor in self.processors]
  52. def get_processor(self, name: str) -> typing.Optional[ACMProfileProcessor]:
  53. for processor in self.processors:
  54. if name == processor.name:
  55. return processor
  56. return None
  57. class ACMS3(BaseModel):
  58. secure: bool = False,
  59. host: str = "127.0.0.1:9000"
  60. access_key: typing.Optional[str]
  61. secret_key: typing.Optional[str]
  62. class ACMConfig(BaseSettings):
  63. concurrency: int = 0
  64. debug: bool = False
  65. s3: typing.Optional[ACMS3]
  66. version: typing.Optional[str]
  67. profiles: typing.List[ACMProfile]
  68. signature: typing.Optional[str]
  69. @validator('version', always=True)
  70. def version_validator(cls, v, values) -> str:
  71. if v is None:
  72. return VERSION
  73. @validator('signature', always=True)
  74. def signature_validator(cls, v, values) -> str:
  75. signature_keys = ["version"]
  76. signature_values = [value for key, value in values.items() if key in signature_keys]
  77. signature = get_string_sha256sum(json.dumps(signature_values))
  78. profiles_signatures = [profiles.signature for profiles in values["profiles"]]
  79. if len(profiles_signatures) > 1:
  80. combined_profiles_signature = get_string_xor(*profiles_signatures)
  81. else:
  82. combined_profiles_signature = profiles_signatures[0]
  83. return get_string_sha256sum(signature + combined_profiles_signature)
  84. class Config:
  85. env_prefix = 'ACM_'
  86. env_nested_delimiter = '__'
  87. def get_profile_names(self) -> typing.List[str]:
  88. return [profile.name for profile in self.profiles]
  89. def get_profile(self, name: str) -> typing.Optional[ACMProfile]:
  90. for profile in self.profiles:
  91. if name == profile.name:
  92. return profile
  93. return None
  94. def get_default_config():
  95. """
  96. Returns the default ACM config
  97. """
  98. acm_profiles = []
  99. # default #
  100. acm_default_processors = []
  101. acm_default_processors.append(ACMProfileProcessor(
  102. name = "jpeg",
  103. processors = ["cjpeg"],
  104. extensions = ["jpg", "jpeg"],
  105. output_extension = "jpg",
  106. options = ACMProfileProcessorOptions(force_preserve_smaller_input=True),
  107. command = "cjpeg -optimize -quality 90 -progressive -outfile {output_file} {input_file}"
  108. ))
  109. acm_default_processors.append(ACMProfileProcessor(
  110. name = "png",
  111. processors = ["optipng"],
  112. extensions = ["png"],
  113. output_extension = "png",
  114. options = ACMProfileProcessorOptions(force_preserve_smaller_input=True),
  115. command = "optipng -o2 -strip all -out {output_file} {input_file}"
  116. ))
  117. acm_default_processors.append(ACMProfileProcessor(
  118. name = "video",
  119. processors = ["ffmpeg"],
  120. extensions = ["mp4","webm"],
  121. output_extension = "webm",
  122. options = ACMProfileProcessorOptions(),
  123. command = "optipng -o2 -strip all -out {output_file} {input_file}"
  124. ))
  125. acm_default_processors.append(ACMProfileProcessor(
  126. name = "audio",
  127. processors = ["ffmpeg","opusenc"],
  128. extensions = ["wav","mp3"],
  129. output_extension = "ogg",
  130. options = ACMProfileProcessorOptions(),
  131. command = "optipng -o2 -strip all -out {output_file} {input_file}"
  132. ))
  133. acm_profiles.append(ACMProfile(
  134. name = "default",
  135. processors = acm_default_processors
  136. ))
  137. # placebo #
  138. acm_placebo_processors = []
  139. acm_placebo_processors.append(ACMProfileProcessor(
  140. name = "jpeg",
  141. processors = ["cjpeg"],
  142. extensions = ["jpg", "jpeg"],
  143. output_extension = "jpg",
  144. options = ACMProfileProcessorOptions(),
  145. command = "cp {input_file} {output_file}"
  146. ))
  147. acm_placebo_processors.append(ACMProfileProcessor(
  148. name = "png",
  149. processors = ["optipng"],
  150. extensions = ["png"],
  151. output_extension = "png",
  152. options = ACMProfileProcessorOptions(),
  153. command = "cp {input_file} {output_file}"
  154. ))
  155. acm_placebo_processors.append(ACMProfileProcessor(
  156. name = "video",
  157. processors = ["ffmpeg"],
  158. extensions = ["mp4","webm"],
  159. output_extension = "webm",
  160. options = ACMProfileProcessorOptions(),
  161. command = "cp {input_file} {output_file}"
  162. ))
  163. acm_placebo_processors.append(ACMProfileProcessor(
  164. name = "audio",
  165. processors = ["ffmpeg","opusenc"],
  166. extensions = ["wav","mp3"],
  167. output_extension = "ogg",
  168. options = ACMProfileProcessorOptions(),
  169. command = "cp {input_file} {output_file}"
  170. ))
  171. acm_profiles.append(ACMProfile(
  172. name = "placebo",
  173. processors = acm_placebo_processors
  174. ))
  175. # webp #
  176. acm_webp_processors = []
  177. acm_webp_processors.append(ACMProfileProcessor(
  178. name = "jpeg",
  179. processors = ["cwebp"],
  180. extensions = ["jpg", "jpeg"],
  181. output_extension = "jpg",
  182. options = ACMProfileProcessorOptions(),
  183. command = "cwebp -jpeg_like -q 90 -o {output_file} {input_file}"
  184. ))
  185. acm_webp_processors.append(ACMProfileProcessor(
  186. name = "png",
  187. processors = ["cwebp"],
  188. extensions = ["png"],
  189. output_extension = "png",
  190. options = ACMProfileProcessorOptions(),
  191. command = "cwebp -lossless -o {output_file} {input_file}"
  192. ))
  193. acm_profiles.append(ACMProfile(
  194. name = "webp",
  195. processors = acm_webp_processors
  196. ))
  197. # aggressive #
  198. acm_aggressive_processors = []
  199. acm_aggressive_processors.append(ACMProfileProcessor(
  200. name = "jpeg",
  201. processors = ["cjpeg"],
  202. extensions = ["jpg", "jpeg"],
  203. output_extension = "jpg",
  204. options = ACMProfileProcessorOptions(force_preserve_smaller_input=True),
  205. command = "export FILE={output_file} && export TEMP_FILE=${FILE}_tmp.jpg && ffmpeg -i {input_file} -vf scale=-1:720 ${TEMP_FILE} && cjpeg -optimize -quality 75 -progressive -outfile {output_file} ${TEMP_FILE} && rm ${TEMP_FILE}"
  206. ))
  207. acm_aggressive_processors.append(ACMProfileProcessor(
  208. name = "png",
  209. processors = ["optipng"],
  210. extensions = ["png"],
  211. output_extension = "png",
  212. options = ACMProfileProcessorOptions(force_preserve_smaller_input=True),
  213. command = "optipng -o2 -strip all -out {output_file} {input_file}"
  214. ))
  215. acm_aggressive_processors.append(ACMProfileProcessor(
  216. name = "video",
  217. processors = ["ffmpeg"],
  218. extensions = ["mp4","webm"],
  219. output_extension = "webm",
  220. options = ACMProfileProcessorOptions(),
  221. command = "ffmpeg -hide_banner -loglevel panic -i {input_file} -vf scale=-1:720 -c:v libvpx-vp9 -b:v 0 -crf 38 -c:a libopus {output_file}"
  222. ))
  223. acm_aggressive_processors.append(ACMProfileProcessor(
  224. name = "audio",
  225. processors = ["ffmpeg","opusenc"],
  226. extensions = ["wav","mp3"],
  227. output_extension = "ogg",
  228. options = ACMProfileProcessorOptions(),
  229. command = "ffmpeg -hide_banner -loglevel panic -i {input_file} -f wav -| opusenc --bitrate 64 --vbr --downmix-stereo --discard-comments --discard-pictures - {output_file}"
  230. ))
  231. acm_profiles.append(ACMProfile(
  232. name = "aggressive",
  233. processors = acm_aggressive_processors
  234. ))
  235. # aggressive-webp #
  236. acm_aggressive_webp_processors = []
  237. acm_aggressive_webp_processors.append(ACMProfileProcessor(
  238. name = "jpeg",
  239. processors = ["cwebp"],
  240. extensions = ["jpg", "jpeg"],
  241. output_extension = "jpg",
  242. options = ACMProfileProcessorOptions(),
  243. command = "export FILE={output_file} && export TEMP_FILE=${FILE}_tmp.jpg && ffmpeg -i {input_file} -vf scale=-1:720 ${TEMP_FILE} && cwebp -jpeg_like -q 75 -o {output_file} ${TEMP_FILE} && rm ${TEMP_FILE}"
  244. ))
  245. acm_aggressive_webp_processors.append(ACMProfileProcessor(
  246. name = "png",
  247. processors = ["optipng"],
  248. extensions = ["png"],
  249. output_extension = "png",
  250. options = ACMProfileProcessorOptions(),
  251. command = "cwebp -o {output_file} ${input_file}"
  252. ))
  253. acm_aggressive_webp_processors.append(ACMProfileProcessor(
  254. name = "video",
  255. processors = ["ffmpeg"],
  256. extensions = ["mp4","webm"],
  257. output_extension = "webm",
  258. options = ACMProfileProcessorOptions(),
  259. command = "ffmpeg -hide_banner -loglevel panic -i {input_file} -vf scale=-1:720 -c:v libvpx-vp9 -b:v 0 -crf 38 -c:a libopus {output_file}"
  260. ))
  261. acm_aggressive_webp_processors.append(ACMProfileProcessor(
  262. name = "audio",
  263. processors = ["ffmpeg","opusenc"],
  264. extensions = ["wav","mp3"],
  265. output_extension = "ogg",
  266. options = ACMProfileProcessorOptions(),
  267. command = "ffmpeg -hide_banner -loglevel panic -i {input_file} -f wav -| opusenc --bitrate 64 --vbr --downmix-stereo --discard-comments --discard-pictures - {output_file}"
  268. ))
  269. acm_profiles.append(ACMProfile(
  270. name = "aggressive-webp",
  271. processors = acm_aggressive_webp_processors
  272. ))
  273. return ACMConfig(
  274. profiles=acm_profiles
  275. )