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.

297 lines
11 KiB

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