mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 10:58:00 +00:00
Added a configurable Opus bitrate for audio transcription
This commit is contained in:
1 parent
557d0b1409
commit
6be958eb92
14 files changed
+138
-18
No files matched your search
@@ -4993,6 +4993,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T16
|
|||||||
-- Uses the provider-configured model
|
-- Uses the provider-configured model
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1760715963"] = "Uses the provider-configured model"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1760715963"] = "Uses the provider-configured model"
|
||||||
|
|
||||||
|
-- Higher bitrates can improve transcription accuracy, especially for quiet or noisy recordings, at the cost of a larger upload to the transcription provider. 128 kbps is recommended.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1859657826"] = "Higher bitrates can improve transcription accuracy, especially for quiet or noisy recordings, at the cost of a larger upload to the transcription provider. 128 kbps is recommended."
|
||||||
|
|
||||||
-- Add Transcription Provider
|
-- Add Transcription Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Add Transcription Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Add Transcription Provider"
|
||||||
|
|
||||||
@@ -5005,6 +5008,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T21
|
|||||||
-- Name
|
-- Name
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T266367750"] = "Name"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T266367750"] = "Name"
|
||||||
|
|
||||||
|
-- Transcription audio quality
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T3103106744"] = "Transcription audio quality"
|
||||||
|
|
||||||
-- Edit
|
-- Edit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T3267849393"] = "Edit"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T3267849393"] = "Edit"
|
||||||
|
|
||||||
@@ -10492,6 +10498,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T4107955313"]
|
|||||||
-- Always use light theme
|
-- Always use light theme
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T534715610"] = "Always use light theme"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T534715610"] = "Always use light theme"
|
||||||
|
|
||||||
|
-- 128 kbps (recommended)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T2152168180"] = "128 kbps (recommended)"
|
||||||
|
|
||||||
|
-- 256 kbps (largest upload, highest accuracy)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T3092489829"] = "256 kbps (largest upload, highest accuracy)"
|
||||||
|
|
||||||
|
-- Unknown
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T3424652889"] = "Unknown"
|
||||||
|
|
||||||
|
-- 64 kbps
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T3501477553"] = "64 kbps"
|
||||||
|
|
||||||
|
-- 32 kbps (smallest upload, lowest accuracy)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T767394292"] = "32 kbps (smallest upload, lowest accuracy)"
|
||||||
|
|
||||||
-- Use no profile
|
-- Use no profile
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::PROFILE::T2205839602"] = "Use no profile"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::PROFILE::T2205839602"] = "Use no profile"
|
||||||
|
|
||||||
|
|||||||
@@ -84,5 +84,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<LockableButton Text="@T("Add transcription provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider || !this.SettingsManager.ConfigurationData.App.AllowUserToAddTranscriptionProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddTranscriptionProvider" Class="mt-3" />
|
<LockableButton Text="@T("Add transcription provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider || !this.SettingsManager.ConfigurationData.App.AllowUserToAddTranscriptionProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddTranscriptionProvider" Class="mt-3" />
|
||||||
|
|
||||||
|
<MudDivider Class="mt-6 mb-3"/>
|
||||||
|
|
||||||
|
<ConfigurationSelect OptionDescription="@T("Transcription audio quality")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.OpusBitrate)" Data="@ConfigurationSelectDataFactory.GetTranscriptionOpusBitrateData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.OpusBitrate = selectedValue)" OptionHelp="@this.OpusBitrateHelp" IsLocked="@this.IsOpusBitrateLocked"/>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,10 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase
|
|||||||
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
|
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string OpusBitrateHelp => T("Higher bitrates can improve transcription accuracy, especially for quiet or noisy recordings, at the cost of a larger upload to the transcription provider. 128 kbps is recommended.");
|
||||||
|
|
||||||
|
private bool IsOpusBitrateLocked() => ManagedConfiguration.TryGet(x => x.App, x => x.OpusBitrate, out var meta) && meta.IsLocked;
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
|
|||||||
@@ -578,6 +578,15 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- Please note: using an empty string ("") will lock the selection and disable dictation/transcription.
|
-- Please note: using an empty string ("") will lock the selection and disable dictation/transcription.
|
||||||
-- CONFIG["SETTINGS"]["DataApp.UseTranscriptionProvider"] = "00000000-0000-0000-0000-000000000000"
|
-- CONFIG["SETTINGS"]["DataApp.UseTranscriptionProvider"] = "00000000-0000-0000-0000-000000000000"
|
||||||
|
|
||||||
|
-- Configure the Opus bitrate used when normalizing uploaded audio/video for transcription.
|
||||||
|
-- Allowed values are: KBPS_32, KBPS_64, KBPS_128, KBPS_256
|
||||||
|
-- Higher bitrates improve transcription accuracy on noisy or quiet recordings, at the cost of
|
||||||
|
-- a larger upload to the transcription provider. KBPS_128 is recommended.
|
||||||
|
-- CONFIG["SETTINGS"]["DataApp.OpusBitrate"] = "KBPS_32"
|
||||||
|
--
|
||||||
|
-- Allow the user to change the Opus bitrate even though your organization set a default above:
|
||||||
|
-- CONFIG["SETTINGS"]["DataApp.OpusBitrate.AllowUserOverride"] = true
|
||||||
|
|
||||||
-- Configure which assistants should be hidden from the UI.
|
-- Configure which assistants should be hidden from the UI.
|
||||||
-- Allowed values are:
|
-- Allowed values are:
|
||||||
-- GRAMMAR_SPELLING_ASSISTANT, ICON_FINDER_ASSISTANT, REWRITE_ASSISTANT,
|
-- GRAMMAR_SPELLING_ASSISTANT, ICON_FINDER_ASSISTANT, REWRITE_ASSISTANT,
|
||||||
|
|||||||
+21
@@ -4995,6 +4995,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T16
|
|||||||
-- Uses the provider-configured model
|
-- Uses the provider-configured model
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1760715963"] = "Verwendet das vom Anbieter konfigurierte Modell"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1760715963"] = "Verwendet das vom Anbieter konfigurierte Modell"
|
||||||
|
|
||||||
|
-- Higher bitrates can improve transcription accuracy, especially for quiet or noisy recordings, at the cost of a larger upload to the transcription provider. 128 kbps is recommended.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1859657826"] = "Höhere Bitraten können die Transkriptionsgenauigkeit verbessern, besonders bei ruhigen oder lauten Aufnahmen, zum Nachteil eines größeren Uploads an den Transkriptionsdienstleister. 128 kbps wird empfohlen."
|
||||||
|
|
||||||
-- Add Transcription Provider
|
-- Add Transcription Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Anbieter für Transkriptionen hinzufügen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Anbieter für Transkriptionen hinzufügen"
|
||||||
|
|
||||||
@@ -5007,6 +5010,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T21
|
|||||||
-- Name
|
-- Name
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T266367750"] = "Name"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T266367750"] = "Name"
|
||||||
|
|
||||||
|
-- Transcription audio quality
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T3103106744"] = "Aufzeichnungsqualität für die Transkription"
|
||||||
|
|
||||||
-- Edit
|
-- Edit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T3267849393"] = "Bearbeiten"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T3267849393"] = "Bearbeiten"
|
||||||
|
|
||||||
@@ -10494,6 +10500,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T4107955313"]
|
|||||||
-- Always use light theme
|
-- Always use light theme
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T534715610"] = "Immer das helle Design verwenden"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T534715610"] = "Immer das helle Design verwenden"
|
||||||
|
|
||||||
|
-- 128 kbps (recommended)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T2152168180"] = "128 kbps (empfohlen)"
|
||||||
|
|
||||||
|
-- 256 kbps (largest upload, highest accuracy)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T3092489829"] = "256 kbps (größter Upload, höchste Genauigkeit)"
|
||||||
|
|
||||||
|
-- Unknown
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T3424652889"] = "Bitte geben Sie den zu übersetzenden Text an."
|
||||||
|
|
||||||
|
-- 64 kbps
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T3501477553"] = "64 kbps"
|
||||||
|
|
||||||
|
-- 32 kbps (smallest upload, lowest accuracy)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::TRANSCRIPTIONOPUSBITRATEEXTENSIONS::T767394292"] = "32 kbps (kleinster Upload, geringste Genauigkeit)"
|
||||||
|
|
||||||
-- Use no profile
|
-- Use no profile
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::PROFILE::T2205839602"] = "Kein Profil verwenden"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::PROFILE::T2205839602"] = "Kein Profil verwenden"
|
||||||
|
|
||||||
|
|||||||
@@ -323,4 +323,10 @@ public static class ConfigurationSelectDataFactory
|
|||||||
yield return new(level.GetName(), level);
|
yield return new(level.GetName(), level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<ConfigurationSelectData<TranscriptionOpusBitrate>> GetTranscriptionOpusBitrateData()
|
||||||
|
{
|
||||||
|
foreach (var bitrate in Enum.GetValues<TranscriptionOpusBitrate>())
|
||||||
|
yield return new(bitrate.GetName(), bitrate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -112,6 +112,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string UseTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.UseTranscriptionProvider, string.Empty);
|
public string UseTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.UseTranscriptionProvider, string.Empty);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Opus bitrate used when normalizing uploaded audio/video for transcription.
|
||||||
|
/// </summary>
|
||||||
|
public TranscriptionOpusBitrate OpusBitrate { get; set; } = ManagedConfiguration.Register(configSelection, n => n.OpusBitrate, TranscriptionOpusBitrate.KBPS_128);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The global keyboard shortcut for toggling voice recording.
|
/// The global keyboard shortcut for toggling voice recording.
|
||||||
/// Uses Tauri's shortcut format, e.g., "CmdOrControl+1" (Cmd+1 on macOS, Ctrl+1 on Windows/Linux).
|
/// Uses Tauri's shortcut format, e.g., "CmdOrControl+1" (Cmd+1 on macOS, Ctrl+1 on Windows/Linux).
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
|
public enum TranscriptionOpusBitrate
|
||||||
|
{
|
||||||
|
KBPS_32,
|
||||||
|
KBPS_64,
|
||||||
|
KBPS_128,
|
||||||
|
KBPS_256,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
namespace AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
|
public static class TranscriptionOpusBitrateExtensions
|
||||||
|
{
|
||||||
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(TranscriptionOpusBitrateExtensions).Namespace, nameof(TranscriptionOpusBitrateExtensions));
|
||||||
|
|
||||||
|
public static string GetName(this TranscriptionOpusBitrate bitrate) => bitrate switch
|
||||||
|
{
|
||||||
|
TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"),
|
||||||
|
TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"),
|
||||||
|
TranscriptionOpusBitrate.KBPS_128 => TB("128 kbps (recommended)"),
|
||||||
|
TranscriptionOpusBitrate.KBPS_256 => TB("256 kbps (largest upload, highest accuracy)"),
|
||||||
|
_ => TB("Unknown"),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static uint GetBitsPerSecond(this TranscriptionOpusBitrate bitrate) => bitrate switch
|
||||||
|
{
|
||||||
|
TranscriptionOpusBitrate.KBPS_32 => 32_000,
|
||||||
|
TranscriptionOpusBitrate.KBPS_64 => 64_000,
|
||||||
|
TranscriptionOpusBitrate.KBPS_128 => 128_000,
|
||||||
|
TranscriptionOpusBitrate.KBPS_256 => 256_000,
|
||||||
|
_ => 32_000,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -398,6 +398,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
// Config: transcription provider?
|
// Config: transcription provider?
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UseTranscriptionProvider, Guid.Empty, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UseTranscriptionProvider, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
|
// Config: transcription Opus bitrate?
|
||||||
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.OpusBitrate, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
message = string.Empty;
|
message = string.Empty;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ namespace AIStudio.Tools.Rust;
|
|||||||
/// <param name="InputPath">Absolute source media path.</param>
|
/// <param name="InputPath">Absolute source media path.</param>
|
||||||
/// <param name="OutputPath">Absolute operation-owned output path.</param>
|
/// <param name="OutputPath">Absolute operation-owned output path.</param>
|
||||||
/// <param name="MaxPassThroughBytes">Optional pass-through size ceiling.</param>
|
/// <param name="MaxPassThroughBytes">Optional pass-through size ceiling.</param>
|
||||||
public sealed record CreateMediaJobRequest(string InputPath, string OutputPath, ulong? MaxPassThroughBytes = null);
|
/// <param name="OpusBitrateBps">Optional target Opus encoder bitrate in bits per second.</param>
|
||||||
|
public sealed record CreateMediaJobRequest(string InputPath, string OutputPath, ulong? MaxPassThroughBytes = null, uint? OpusBitrateBps = null);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
using AIStudio.Tools.Media;
|
using AIStudio.Tools.Media;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
using AIStudio.Tools.Rust;
|
using AIStudio.Tools.Rust;
|
||||||
@@ -648,7 +649,8 @@ public sealed class MediaTranscriptionService(
|
|||||||
{
|
{
|
||||||
// The quick POST is intentionally not cancelled: losing its response could orphan a job
|
// The quick POST is intentionally not cancelled: losing its response could orphan a job
|
||||||
// whose ID the client never received. Cancellation is applied immediately after ownership.
|
// whose ID the client never received. Cancellation is applied immediately after ownership.
|
||||||
var jobId = await rustService.StartMediaJobAsync(mediaPath, normalizedPath, CancellationToken.None);
|
var opusBitrateBps = settingsManager.ConfigurationData.App.OpusBitrate.GetBitsPerSecond();
|
||||||
|
var jobId = await rustService.StartMediaJobAsync(mediaPath, normalizedPath, opusBitrateBps, CancellationToken.None);
|
||||||
operation.JobId = jobId;
|
operation.JobId = jobId;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ public partial class RustService
|
|||||||
/// <summary>Starts a Rust media normalization job.</summary>
|
/// <summary>Starts a Rust media normalization job.</summary>
|
||||||
/// <param name="inputPath">Absolute source path.</param>
|
/// <param name="inputPath">Absolute source path.</param>
|
||||||
/// <param name="outputPath">Absolute operation-owned output path.</param>
|
/// <param name="outputPath">Absolute operation-owned output path.</param>
|
||||||
|
/// <param name="opusBitrateBps">Target Opus encoder bitrate in bits per second.</param>
|
||||||
/// <param name="token">Request cancellation token.</param>
|
/// <param name="token">Request cancellation token.</param>
|
||||||
/// <returns>The opaque runtime job identifier.</returns>
|
/// <returns>The opaque runtime job identifier.</returns>
|
||||||
public async Task<string> StartMediaJobAsync(string inputPath, string outputPath, CancellationToken token = default)
|
public async Task<string> StartMediaJobAsync(string inputPath, string outputPath, uint opusBitrateBps, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
using var response = await this.http.PostAsJsonAsync(
|
using var response = await this.http.PostAsJsonAsync(
|
||||||
"/media/jobs",
|
"/media/jobs",
|
||||||
new CreateMediaJobRequest(inputPath, outputPath),
|
new CreateMediaJobRequest(inputPath, outputPath, OpusBitrateBps: opusBitrateBps),
|
||||||
this.jsonRustSerializerOptions,
|
this.jsonRustSerializerOptions,
|
||||||
token);
|
token);
|
||||||
|
|
||||||
|
|||||||
+22
-14
@@ -44,9 +44,6 @@ const OUTPUT_SAMPLE_RATE: u32 = 48_000;
|
|||||||
/// Number of samples in one 20 ms Opus frame at 48 kHz.
|
/// Number of samples in one 20 ms Opus frame at 48 kHz.
|
||||||
const OPUS_FRAME_SAMPLES: usize = 960;
|
const OPUS_FRAME_SAMPLES: usize = 960;
|
||||||
|
|
||||||
/// Target bitrate for mono speech-oriented Opus output.
|
|
||||||
const OPUS_BITRATE: u32 = 32_000;
|
|
||||||
|
|
||||||
/// Stable normalized container name returned to upload clients.
|
/// Stable normalized container name returned to upload clients.
|
||||||
const OUTPUT_FORMAT: &str = "webm";
|
const OUTPUT_FORMAT: &str = "webm";
|
||||||
|
|
||||||
@@ -62,6 +59,9 @@ const OPUS_PRE_SKIP: u16 = 312;
|
|||||||
/// Default size ceiling for copying an already-normalized file unchanged.
|
/// Default size ceiling for copying an already-normalized file unchanged.
|
||||||
const DEFAULT_MAX_PASS_THROUGH_BYTES: u64 = 25 * 1024 * 1024;
|
const DEFAULT_MAX_PASS_THROUGH_BYTES: u64 = 25 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Default target bitrate for mono speech-oriented Opus output, used when a request omits it.
|
||||||
|
const DEFAULT_OPUS_BITRATE_BPS: u32 = 128_000;
|
||||||
|
|
||||||
/// Bounded input block used for streaming resampling.
|
/// Bounded input block used for streaming resampling.
|
||||||
const RESAMPLE_INPUT_BLOCK_SAMPLES: usize = 2_048;
|
const RESAMPLE_INPUT_BLOCK_SAMPLES: usize = 2_048;
|
||||||
|
|
||||||
@@ -94,6 +94,9 @@ pub struct CreateMediaJobRequest {
|
|||||||
|
|
||||||
/// Optional size ceiling for pass-through files.
|
/// Optional size ceiling for pass-through files.
|
||||||
pub max_pass_through_bytes: Option<u64>,
|
pub max_pass_through_bytes: Option<u64>,
|
||||||
|
|
||||||
|
/// Optional target Opus encoder bitrate in bits per second.
|
||||||
|
pub opus_bitrate_bps: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response returned immediately after a media job has been registered.
|
/// Response returned immediately after a media job has been registered.
|
||||||
@@ -348,8 +351,9 @@ pub async fn create_job(
|
|||||||
let started_at = Instant::now();
|
let started_at = Instant::now();
|
||||||
log::info!("media job registered: job_id={completed_job_id}");
|
log::info!("media job registered: job_id={completed_job_id}");
|
||||||
let max_pass_through_bytes = request.max_pass_through_bytes.unwrap_or(DEFAULT_MAX_PASS_THROUGH_BYTES);
|
let max_pass_through_bytes = request.max_pass_through_bytes.unwrap_or(DEFAULT_MAX_PASS_THROUGH_BYTES);
|
||||||
|
let opus_bitrate_bps = request.opus_bitrate_bps.unwrap_or(DEFAULT_OPUS_BITRATE_BPS);
|
||||||
let task_job = Arc::clone(&job);
|
let task_job = Arc::clone(&job);
|
||||||
let result = tokio::task::spawn_blocking(move || normalize_media(&input_path, &output_path, max_pass_through_bytes, &task_job)).await;
|
let result = tokio::task::spawn_blocking(move || normalize_media(&input_path, &output_path, max_pass_through_bytes, opus_bitrate_bps, &task_job)).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(result)) => {
|
Ok(Ok(result)) => {
|
||||||
log::info!("media job completed: job_id={completed_job_id}, elapsed_ms={}", started_at.elapsed().as_millis());
|
log::info!("media job completed: job_id={completed_job_id}, elapsed_ms={}", started_at.elapsed().as_millis());
|
||||||
@@ -515,7 +519,7 @@ impl MediaSource for CancellationMediaSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Probes, normalizes, and atomically commits one media file.
|
/// Probes, normalizes, and atomically commits one media file.
|
||||||
fn normalize_media(input_path: &FilePath, output_path: &FilePath, max_pass_through_bytes: u64, job: &MediaJob) -> Result<MediaJobResult, MediaError> {
|
fn normalize_media(input_path: &FilePath, output_path: &FilePath, max_pass_through_bytes: u64, opus_bitrate_bps: u32, job: &MediaJob) -> Result<MediaJobResult, MediaError> {
|
||||||
check_cancelled(job)?;
|
check_cancelled(job)?;
|
||||||
|
|
||||||
let detected = FileFormat::from_file(input_path)
|
let detected = FileFormat::from_file(input_path)
|
||||||
@@ -670,6 +674,7 @@ fn normalize_media(input_path: &FilePath, output_path: &FilePath, max_pass_throu
|
|||||||
time_base: track_time_base,
|
time_base: track_time_base,
|
||||||
source_progress,
|
source_progress,
|
||||||
job,
|
job,
|
||||||
|
opus_bitrate_bps,
|
||||||
};
|
};
|
||||||
transcode(&mut *format, context)
|
transcode(&mut *format, context)
|
||||||
};
|
};
|
||||||
@@ -892,6 +897,9 @@ struct TranscodeContext<'a> {
|
|||||||
|
|
||||||
/// Cancellation and progress state for the job.
|
/// Cancellation and progress state for the job.
|
||||||
job: &'a MediaJob,
|
job: &'a MediaJob,
|
||||||
|
|
||||||
|
/// Target Opus encoder bitrate in bits per second.
|
||||||
|
opus_bitrate_bps: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes a selected track and writes timestamp-aligned 20 ms mono Opus frames.
|
/// Decodes a selected track and writes timestamp-aligned 20 ms mono Opus frames.
|
||||||
@@ -901,7 +909,7 @@ fn transcode(
|
|||||||
) -> Result<MediaJobResult, MediaError> {
|
) -> Result<MediaJobResult, MediaError> {
|
||||||
let mut decoder = StreamDecoder::new(&context.params, context.track_delay)?;
|
let mut decoder = StreamDecoder::new(&context.params, context.track_delay)?;
|
||||||
let mut opus_encoder = OpusEncoder::builder(OUTPUT_SAMPLE_RATE, OpusChannels::Mono, Application::Audio)
|
let mut opus_encoder = OpusEncoder::builder(OUTPUT_SAMPLE_RATE, OpusChannels::Mono, Application::Audio)
|
||||||
.bitrate(Bitrate::Bits(OPUS_BITRATE))
|
.bitrate(Bitrate::Bits(context.opus_bitrate_bps))
|
||||||
.vbr(true)
|
.vbr(true)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|error| MediaError::new(MediaErrorCode::EncoderInitFailed, error.to_string()))?;
|
.map_err(|error| MediaError::new(MediaErrorCode::EncoderInitFailed, error.to_string()))?;
|
||||||
@@ -1647,7 +1655,7 @@ mod tests {
|
|||||||
/// Creates a temporary output and normalizes one checked-in fixture.
|
/// Creates a temporary output and normalizes one checked-in fixture.
|
||||||
fn normalize_fixture(name: &str) -> Result<(MediaJobResult, PathBuf), MediaError> {
|
fn normalize_fixture(name: &str) -> Result<(MediaJobResult, PathBuf), MediaError> {
|
||||||
let output = std::env::temp_dir().join(format!("ai-studio-fixture-{}.webm", rand::random::<u64>()));
|
let output = std::env::temp_dir().join(format!("ai-studio-fixture-{}.webm", rand::random::<u64>()));
|
||||||
let result = normalize_media(&fixtures().join(name), &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new())?;
|
let result = normalize_media(&fixtures().join(name), &output, DEFAULT_MAX_PASS_THROUGH_BYTES, DEFAULT_OPUS_BITRATE_BPS, &MediaJob::new())?;
|
||||||
Ok((result, output))
|
Ok((result, output))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1804,7 +1812,7 @@ mod tests {
|
|||||||
let output = directory.join("output.webm");
|
let output = directory.join("output.webm");
|
||||||
fs::write(&input, wav_silence(44_100, 4_410)).unwrap();
|
fs::write(&input, wav_silence(44_100, 4_410)).unwrap();
|
||||||
let job = MediaJob::new();
|
let job = MediaJob::new();
|
||||||
let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &job).unwrap();
|
let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, DEFAULT_OPUS_BITRATE_BPS, &job).unwrap();
|
||||||
assert!(!result.pass_through);
|
assert!(!result.pass_through);
|
||||||
assert_eq!(result.output_format, OUTPUT_FORMAT);
|
assert_eq!(result.output_format, OUTPUT_FORMAT);
|
||||||
assert_eq!(result.output_codec, OUTPUT_CODEC);
|
assert_eq!(result.output_codec, OUTPUT_CODEC);
|
||||||
@@ -1832,7 +1840,7 @@ mod tests {
|
|||||||
fs::write(&input, wav_silence(48_000, 960)).unwrap();
|
fs::write(&input, wav_silence(48_000, 960)).unwrap();
|
||||||
let job = MediaJob::new();
|
let job = MediaJob::new();
|
||||||
job.cancelled.store(true, Ordering::Relaxed);
|
job.cancelled.store(true, Ordering::Relaxed);
|
||||||
let error = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &job).unwrap_err();
|
let error = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, DEFAULT_OPUS_BITRATE_BPS, &job).unwrap_err();
|
||||||
assert_eq!(error.code, MediaErrorCode::Cancelled);
|
assert_eq!(error.code, MediaErrorCode::Cancelled);
|
||||||
assert!(!output.exists());
|
assert!(!output.exists());
|
||||||
assert!(!partial_path(&output).exists());
|
assert!(!partial_path(&output).exists());
|
||||||
@@ -1850,7 +1858,7 @@ mod tests {
|
|||||||
writer.write_packet(&[0xf8, 0xff, 0xfe], 0).unwrap();
|
writer.write_packet(&[0xf8, 0xff, 0xfe], 0).unwrap();
|
||||||
writer.finish().unwrap();
|
writer.finish().unwrap();
|
||||||
let job = MediaJob::new();
|
let job = MediaJob::new();
|
||||||
let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &job).unwrap();
|
let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, DEFAULT_OPUS_BITRATE_BPS, &job).unwrap();
|
||||||
assert!(result.pass_through);
|
assert!(result.pass_through);
|
||||||
assert_eq!(result.output_format, OUTPUT_FORMAT);
|
assert_eq!(result.output_format, OUTPUT_FORMAT);
|
||||||
assert_eq!(result.output_codec, OUTPUT_CODEC);
|
assert_eq!(result.output_codec, OUTPUT_CODEC);
|
||||||
@@ -1867,7 +1875,7 @@ mod tests {
|
|||||||
let input = directory.join("input.wav");
|
let input = directory.join("input.wav");
|
||||||
let output = directory.join("output.webm");
|
let output = directory.join("output.webm");
|
||||||
fs::write(&input, wav_constant(48_000, 960, 1_000)).unwrap();
|
fs::write(&input, wav_constant(48_000, 960, 1_000)).unwrap();
|
||||||
let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new()).unwrap();
|
let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, DEFAULT_OPUS_BITRATE_BPS, &MediaJob::new()).unwrap();
|
||||||
assert!(result.has_audible_signal);
|
assert!(result.has_audible_signal);
|
||||||
let _ = fs::remove_dir_all(directory);
|
let _ = fs::remove_dir_all(directory);
|
||||||
}
|
}
|
||||||
@@ -1914,15 +1922,15 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fixture_errors_are_stable() {
|
fn fixture_errors_are_stable() {
|
||||||
let damaged_output = std::env::temp_dir().join(format!("ai-studio-damaged-{}.webm", rand::random::<u64>()));
|
let damaged_output = std::env::temp_dir().join(format!("ai-studio-damaged-{}.webm", rand::random::<u64>()));
|
||||||
let damaged = normalize_media(&fixtures().join("damaged.bin"), &damaged_output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new()).unwrap_err();
|
let damaged = normalize_media(&fixtures().join("damaged.bin"), &damaged_output, DEFAULT_MAX_PASS_THROUGH_BYTES, DEFAULT_OPUS_BITRATE_BPS, &MediaJob::new()).unwrap_err();
|
||||||
assert!(matches!(damaged.code, MediaErrorCode::UnknownFormat | MediaErrorCode::NotMedia | MediaErrorCode::DamagedContainer));
|
assert!(matches!(damaged.code, MediaErrorCode::UnknownFormat | MediaErrorCode::NotMedia | MediaErrorCode::DamagedContainer));
|
||||||
|
|
||||||
let no_audio_output = std::env::temp_dir().join(format!("ai-studio-no-audio-{}.webm", rand::random::<u64>()));
|
let no_audio_output = std::env::temp_dir().join(format!("ai-studio-no-audio-{}.webm", rand::random::<u64>()));
|
||||||
let no_audio = normalize_media(&fixtures().join("no-audio.webm"), &no_audio_output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new()).unwrap_err();
|
let no_audio = normalize_media(&fixtures().join("no-audio.webm"), &no_audio_output, DEFAULT_MAX_PASS_THROUGH_BYTES, DEFAULT_OPUS_BITRATE_BPS, &MediaJob::new()).unwrap_err();
|
||||||
assert_eq!(no_audio.code, MediaErrorCode::NoAudioTrack);
|
assert_eq!(no_audio.code, MediaErrorCode::NoAudioTrack);
|
||||||
|
|
||||||
let unknown_output = std::env::temp_dir().join(format!("ai-studio-unknown-{}.webm", rand::random::<u64>()));
|
let unknown_output = std::env::temp_dir().join(format!("ai-studio-unknown-{}.webm", rand::random::<u64>()));
|
||||||
let unknown = normalize_media(&fixtures().join("unknown-codec.mkv"), &unknown_output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new()).unwrap_err();
|
let unknown = normalize_media(&fixtures().join("unknown-codec.mkv"), &unknown_output, DEFAULT_MAX_PASS_THROUGH_BYTES, DEFAULT_OPUS_BITRATE_BPS, &MediaJob::new()).unwrap_err();
|
||||||
assert_eq!(unknown.code, MediaErrorCode::UnsupportedCodec);
|
assert_eq!(unknown.code, MediaErrorCode::UnsupportedCodec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user