Avoiding discontinuities is a bit tricky for reasons outlined in this Q&A - if we try to strictly control, say, the up direction to align as closely as possible with world up, then we'll have to flip somewhere if our rotation does a vertical loop-de-loop, reversing its relationship with the world up axis suddenly.
Instead, let's attach a reference "up" vector to each interpolated point, perpendicular to current local forward vector, and we'll try to align it with world up when we can.
You can choose your first point's reference "up" vector arbitrarily (eg. the local up vector of the initial rotation, so your twist angle starts at zero)
For each subsequent point, we can choose our reference vector as:
sourceUp = previousReferenceUp + untwist * dot(previousReferenceUp, worldUp) * worldUp;
referenceRight = normalize(cross(sourceUp, localForward));
referenceUp = cross(localForward, referenceRight);
Here I'm assuming left-handed coordinate system, with untwist
being a small tuning coefficient (eg. 0.01) to to control how much our reference up seeks vertical in non-vertical sections.
Now we can compute an angular twist about the forward axis relative to this referenceUp
direction as:
localUp = rotation * worldUp;
angle = atan2(-dot(referenceRight, localUp), dot(referenceUp, localUp));
Our twist quaternion can then be expressed as:
// Remember to convert to degrees if expected by your Quaternion library (eg. Unity's)
twist = Quaternion.AngleAxis(angle, worldForward);
We can compute its inverse (ie. negate the x, y, z components):
untwist = Quaternion.Inverse(twist);
And then compute our untwisted remaining rotation as:
untwisted = rotation * untwist;
Then for any vector v, the result of rotating that vector by our twist angle then by the remaining untwisted rotation is equivalent to rotating the vector by our original quaternion:
rotation * v == untwisted * twist * v;